{"uuid": "68c84e01-1916-4fc4-8d4f-63a2455084c5", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2023-4863", "type": "seen", "source": "https://gist.github.com/tu-trinh-scale/e01154513e08c00742a413f2fb38cbc8", "content": "diff --git a/.bumpversion.cfg b/.bumpversion.cfg\nindex 63edf6812..23de6e95a 100644\n--- a/.bumpversion.cfg\n+++ b/.bumpversion.cfg\n@@ -1,5 +1,5 @@\n [bumpversion]\n-current_version = 2.5.2\n+current_version = 3.1.0\n commit = True\n message = Release v{new_version}\n tag = True\ndiff --git a/.flake8 b/.flake8\nindex 34a829a4b..6c4dd923e 100644\n--- a/.flake8\n+++ b/.flake8\n@@ -4,6 +4,7 @@ exclude = .*,__pycache__,resources.py\n # B008: Do not perform calls in argument defaults. (fine with some Qt stuff)\n # B011: Do not call assert False since python -O removes these calls. Instead\n #       callers should raise AssertionError().\n+# B028: Missing stacklevel= for warnings\n # B305: .next() (false-positives)\n # E128: continuation line under-indented for visual indent\n # E226: missing whitespace around arithmetic operator\n@@ -57,17 +58,16 @@ ignore =\n     PT004,\n     PT011,\n     PT012\n-min-version = 3.7.0\n+min-version = 3.8.0\n max-complexity = 12\n per-file-ignores =\n     qutebrowser/api/hook.py : N801\n-    tests/* : B011,D100,D101\n+    qutebrowser/qt/*.py : F403\n+    tests/* : B011,B028,D100,D101\n     tests/unit/browser/test_history.py : D100,D101,N806\n     tests/helpers/fixtures.py : D100,D101,N806\n     tests/unit/browser/webkit/http/test_content_disposition.py : D100,D101,D400\n copyright-check = True\n copyright-regexp = # Copyright [\\d-]+ .*\n copyright-min-file-size = 110\n-pytest-fixture-no-parentheses = True\n-pytest-mark-no-parentheses = True\n pytest-parametrize-names-type = csv\ndiff --git a/.github/CONTRIBUTING.asciidoc b/.github/CONTRIBUTING.asciidoc\nindex 0d03af336..9c119baa3 100644\n--- a/.github/CONTRIBUTING.asciidoc\n+++ b/.github/CONTRIBUTING.asciidoc\n@@ -9,7 +9,7 @@ open pull requests.\n   pull request page after pushing changes.\n \n - If you are stuck somewhere or have questions,\n-  https://github.com/qutebrowser/qutebrowser/blob/master/doc/help/index.asciidoc#getting-help[please ask]!\n+  https://github.com/qutebrowser/qutebrowser/blob/main/doc/help/index.asciidoc#getting-help[please ask]!\n \n See the link:../doc/contributing.asciidoc[full contribution documentation] for\n details and other useful hints.\ndiff --git a/.github/FUNDING.yml b/.github/FUNDING.yml\nindex 4faa45afb..65ab0afa3 100644\n--- a/.github/FUNDING.yml\n+++ b/.github/FUNDING.yml\n@@ -1,2 +1,2 @@\n github: The-Compiler\n-custom: https://github.com/qutebrowser/qutebrowser/blob/master/README.asciidoc#donating\n+custom: https://github.com/qutebrowser/qutebrowser/blob/main/README.asciidoc#donating\ndiff --git a/.github/SECURITY.md b/.github/SECURITY.md\nindex 7df41b38e..a523b9bdb 100644\n--- a/.github/SECURITY.md\n+++ b/.github/SECURITY.md\n@@ -1 +1,4 @@\n Please report security bugs to [security@qutebrowser.org](mailto:security@qutebrowser.org).\n+(or if GPG encryption is desired, contact me@the-compiler.org with GPG ID [0x916EB0C8FD55A072](https://www.the-compiler.org/pubkey.asc)).\n+\n+Alternatively, [report a vulnerability](https://github.com/qutebrowser/qutebrowser/security/advisories/new) via GitHub's [private reporting feature](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability).\ndiff --git a/.github/dependabot.yml b/.github/dependabot.yml\nnew file mode 100644\nindex 000000000..5ace4600a\n--- /dev/null\n+++ b/.github/dependabot.yml\n@@ -0,0 +1,6 @@\n+version: 2\n+updates:\n+  - package-ecosystem: \"github-actions\"\n+    directory: \"/\"\n+    schedule:\n+      interval: \"weekly\"\ndiff --git a/.github/workflows/bleeding.yml b/.github/workflows/bleeding.yml\nindex 586b3b79e..47264b2e5 100644\n--- a/.github/workflows/bleeding.yml\n+++ b/.github/workflows/bleeding.yml\n@@ -11,26 +11,37 @@ jobs:\n   tests:\n     if: \"github.repository == 'qutebrowser/qutebrowser'\"\n     runs-on: ubuntu-20.04\n-    timeout-minutes: 30\n+    timeout-minutes: 45\n+    strategy:\n+      fail-fast: false\n+      matrix:\n+        include:\n+          - testenv: bleeding\n+            image: \"archlinux-webengine-unstable-qt6\"\n+          - testenv: bleeding-qt5\n+            image: \"archlinux-webengine-unstable\"\n     container:\n-      image: \"qutebrowser/ci:archlinux-webengine-unstable\"\n+      image: \"qutebrowser/ci:${{ matrix.image }}\"\n       env:\n         FORCE_COLOR: \"1\"\n         PY_COLORS: \"1\"\n-        DOCKER: \"archlinux-webengine-unstable\"\n+        DOCKER: \"${{ matrix.image }}\"\n         CI: true\n       volumes:\n         # Hardcoded because we can't use ${{ runner.temp }} here apparently.\n         - /home/runner/work/_temp/:/home/runner/work/_temp/\n       options: --privileged --tty\n     steps:\n-      - uses: actions/checkout@v3\n+      - uses: actions/checkout@v4\n         with:\n           persist-credentials: false\n       - name: Set up problem matchers\n         run: \"python scripts/dev/ci/problemmatchers.py py3 ${{ runner.temp }}\"\n+      - name: Upgrade 3rd party assets\n+        run: \"tox exec -e ${{ matrix.testenv }} -- python scripts/dev/update_3rdparty.py --gh-token ${{ secrets.GITHUB_TOKEN }}\"\n+        if: \"endsWith(matrix.image, '-qt6')\"\n       - name: Run tox\n-        run: dbus-run-session tox -e bleeding\n+        run: dbus-run-session tox -e ${{ matrix.testenv }}\n   irc:\n     timeout-minutes: 2\n     continue-on-error: true\ndiff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\nindex 3bebcfbc4..f0d5f9f91 100644\n--- a/.github/workflows/ci.yml\n+++ b/.github/workflows/ci.yml\n@@ -14,14 +14,15 @@ jobs:\n   linters:\n     if: \"!contains(github.event.head_commit.message, '[ci skip]')\"\n     timeout-minutes: 10\n-    runs-on: ubuntu-20.04\n+    runs-on: ubuntu-22.04\n     strategy:\n       fail-fast: false\n       matrix:\n         include:\n           - testenv: pylint\n           - testenv: flake8\n-          - testenv: mypy\n+          - testenv: mypy-pyqt6\n+          - testenv: mypy-pyqt5\n           - testenv: docs\n           - testenv: vulture\n           - testenv: misc\n@@ -32,21 +33,22 @@ jobs:\n             args: \"-f gcc\"  # For problem matchers\n           - testenv: yamllint\n           - testenv: actionlint\n+          - testenv: package\n     steps:\n-      - uses: actions/checkout@v3\n+      - uses: actions/checkout@v4\n         with:\n           persist-credentials: false\n-      - uses: actions/cache@v3\n+      - uses: actions/cache@v4\n         with:\n           path: |\n             .mypy_cache\n             .tox\n             ~/.cache/pip\n           key: \"${{ matrix.testenv }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('scripts/dev/pylint_checkers/qute_pylint/*.py') }}\"\n-      - uses: actions/setup-python@v4\n+      - uses: actions/setup-python@v5\n         with:\n           python-version: '3.10'\n-      - uses: actions/setup-node@v3\n+      - uses: actions/setup-node@v4\n         with:\n           node-version: '16.x'\n         if: \"matrix.testenv == 'eslint'\"\n@@ -54,8 +56,9 @@ jobs:\n         run: \"python scripts/dev/ci/problemmatchers.py ${{ matrix.testenv }} ${{ runner.temp }}\"\n       - name: Install dependencies\n         run: |\n-            [[ ${{ matrix.testenv }} == eslint ]] &amp;&amp; npm install -g eslint\n-            [[ ${{ matrix.testenv }} == docs ]] &amp;&amp; sudo apt-get update &amp;&amp; sudo apt-get install --no-install-recommends asciidoc\n+            [[ ${{ matrix.testenv }} == eslint ]] &amp;&amp; npm install -g 'eslint@&lt;9.0.0'\n+            [[ ${{ matrix.testenv }} == docs ]] &amp;&amp; sudo apt-get update &amp;&amp; sudo apt-get install --no-install-recommends asciidoc libegl1-mesa\n+            [[ ${{ matrix.testenv }} == vulture || ${{ matrix.testenv }} == pylint ]] &amp;&amp; sudo apt-get update &amp;&amp; sudo apt-get install --no-install-recommends libegl1-mesa\n             if [[ ${{ matrix.testenv }} == shellcheck ]]; then\n                 scversion=\"stable\"\n                 bindir=\"$HOME/.local/bin\"\n@@ -73,23 +76,34 @@ jobs:\n             python -m pip install -U pip\n             python -m pip install -U -r misc/requirements/requirements-tox.txt\n       - name: \"Run ${{ matrix.testenv }}\"\n-        run: \"dbus-run-session -- tox -e ${{ matrix.testenv}} -- ${{ matrix.args }}\"\n+        run: |\n+            if [[ -z \"${{ matrix.args }}\" ]]; then\n+                dbus-run-session -- tox -e ${{ matrix.testenv }}\n+            else\n+                dbus-run-session -- tox -e ${{ matrix.testenv }} -- ${{ matrix.args }}\n+            fi\n \n   tests-docker:\n     if: \"!contains(github.event.head_commit.message, '[ci skip]')\"\n-    timeout-minutes: 30\n-    runs-on: ubuntu-20.04\n+    timeout-minutes: 45\n+    runs-on: ubuntu-22.04\n     strategy:\n       fail-fast: false\n       matrix:\n-        image:\n-          - archlinux-webkit\n-          - archlinux-webengine\n-          # - archlinux-webengine-unstable\n+        include:\n+          - testenv: py-qt5\n+            image: archlinux-webkit\n+          - testenv: py-qt5\n+            image: archlinux-webengine\n+          - testenv: py-qt5\n+            image: archlinux-webengine-unstable\n+          - testenv: py\n+            image: archlinux-webengine-qt6\n+          - testenv: py\n+            image: archlinux-webengine-unstable-qt6\n     container:\n       image: \"qutebrowser/ci:${{ matrix.image }}\"\n       env:\n-        QUTE_BDD_WEBENGINE: \"${{ matrix.image != 'archlinux-webkit' }}\"\n         DOCKER: \"${{ matrix.image }}\"\n         CI: true\n         PYTEST_ADDOPTS: \"--color=yes\"\n@@ -98,13 +112,13 @@ jobs:\n         - /home/runner/work/_temp/:/home/runner/work/_temp/\n       options: --privileged --tty\n     steps:\n-      - uses: actions/checkout@v3\n+      - uses: actions/checkout@v4\n         with:\n           persist-credentials: false\n       - name: Set up problem matchers\n-        run: \"python scripts/dev/ci/problemmatchers.py py38 ${{ runner.temp }}\"\n+        run: \"python scripts/dev/ci/problemmatchers.py tests ${{ runner.temp }}\"\n       - name: Run tox\n-        run: dbus-run-session tox -e py\n+        run: \"dbus-run-session -- tox -e ${{ matrix.testenv }}\"\n \n   tests:\n     if: \"!contains(github.event.head_commit.message, '[ci skip]')\"\n@@ -114,50 +128,71 @@ jobs:\n       fail-fast: false\n       matrix:\n         include:\n-          ### PyQt 5.12 (Python 3.7)\n-          - testenv: py37-pyqt512\n+          ### PyQt 5.15.2 (Python 3.8)\n+          - testenv: py38-pyqt5152\n             os: ubuntu-20.04\n-            python: \"3.7\"\n-          ### PyQt 5.13 (Python 3.7)\n-          - testenv: py37-pyqt513\n+            python: \"3.8\"\n+          ### PyQt 5.15 (Python 3.10, with coverage)\n+          # FIXME:qt6\n+          # - testenv: py310-pyqt515-cov\n+          #   os: ubuntu-22.04\n+          #   python: \"3.10\"\n+          ### PyQt 5.15 (Python 3.11)\n+          - testenv: py311-pyqt515\n             os: ubuntu-20.04\n-            python: \"3.7\"\n-          ### PyQt 5.14 (Python 3.8)\n-          - testenv: py38-pyqt514\n+            python: \"3.11\"\n+          ### PyQt 6.2 (Python 3.8)\n+          - testenv: py38-pyqt62\n             os: ubuntu-20.04\n             python: \"3.8\"\n-          ### PyQt 5.15.0 (Python 3.9)\n-          - testenv: py39-pyqt5150\n+          ### PyQt 6.3 (Python 3.8)\n+          - testenv: py38-pyqt63\n+            os: ubuntu-20.04\n+            python: \"3.8\"\n+          ## PyQt 6.4 (Python 3.9)\n+          - testenv: py39-pyqt64\n             os: ubuntu-20.04\n             python: \"3.9\"\n-          ### PyQt 5.15 (Python 3.10, with coverage)\n-          - testenv: py310-pyqt515-cov\n+          ### PyQt 6.5 (Python 3.10)\n+          - testenv: py310-pyqt65\n             os: ubuntu-22.04\n             python: \"3.10\"\n-          ### PyQt 5.15 (Python 3.11)\n-          - testenv: py311-pyqt515\n-            os: ubuntu-20.04\n-            python: \"3.11-dev\"\n-          ### macOS Big Sur: PyQt 5.15 (Python 3.9 to match PyInstaller env)\n-          - testenv: py39-pyqt515\n+          ### PyQt 6.6 (Python 3.11)\n+          - testenv: py311-pyqt66\n+            os: ubuntu-22.04\n+            python: \"3.11\"\n+          ### PyQt 6.6 (Python 3.12)\n+          - testenv: py312-pyqt66\n+            os: ubuntu-22.04\n+            python: \"3.12\"\n+          ### PyQt 6.7 (Python 3.11)\n+          - testenv: py311-pyqt67\n+            os: ubuntu-22.04\n+            python: \"3.11\"\n+          ### PyQt 6.7 (Python 3.12)\n+          - testenv: py312-pyqt67\n+            os: ubuntu-22.04\n+            python: \"3.12\"\n+          ### macOS Big Sur\n+          - testenv: py312-pyqt67\n             os: macos-11\n-            python: \"3.9\"\n+            python: \"3.12\"\n             args: \"tests/unit\"  # Only run unit tests on macOS\n           ### macOS Monterey\n-          - testenv: py39-pyqt515\n+          - testenv: py312-pyqt67\n             os: macos-12\n-            python: \"3.9\"\n+            python: \"3.12\"\n             args: \"tests/unit\"  # Only run unit tests on macOS\n-          ### Windows: PyQt 5.15 (Python 3.9 to match PyInstaller env)\n-          - testenv: py39-pyqt515\n+          ### Windows\n+          - testenv: py312-pyqt67\n             os: windows-2019\n-            python: \"3.9\"\n+            python: \"3.12\"\n     runs-on: \"${{ matrix.os }}\"\n     steps:\n-      - uses: actions/checkout@v3\n+      - uses: actions/checkout@v4\n         with:\n           persist-credentials: false\n-      - uses: actions/cache@v3\n+      - uses: actions/cache@v4\n         with:\n           path: |\n             .mypy_cache\n@@ -165,7 +200,7 @@ jobs:\n             ~/.cache/pip\n           key: \"${{ matrix.testenv }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}\"\n       - name: Set up Python\n-        uses: actions/setup-python@v4\n+        uses: actions/setup-python@v5\n         with:\n           python-version: \"${{ matrix.python }}\"\n       - name: Set up problem matchers\n@@ -173,12 +208,15 @@ jobs:\n       - name: Install apt dependencies\n         run: |\n             sudo apt-get update\n-            sudo apt-get install --no-install-recommends libyaml-dev libegl1-mesa libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0\n+            sudo apt-get install --no-install-recommends libyaml-dev libegl1-mesa libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0\n         if: \"startsWith(matrix.os, 'ubuntu-')\"\n       - name: Install dependencies\n         run: |\n             python -m pip install -U pip\n             python -m pip install -U -r misc/requirements/requirements-tox.txt\n+      - name: Upgrade 3rd party assets\n+        run: \"tox exec -e ${{ matrix.testenv }} -- python scripts/dev/update_3rdparty.py --gh-token ${{ secrets.GITHUB_TOKEN }}\"\n+        if: \"startsWith(matrix.os, 'windows-')\"\n       - name: \"Run ${{ matrix.testenv }}\"\n         run: \"dbus-run-session -- tox -e ${{ matrix.testenv }} -- ${{ matrix.args }}\"\n         if: \"startsWith(matrix.os, 'ubuntu-')\"\n@@ -198,25 +236,25 @@ jobs:\n     if: \"!contains(github.event.head_commit.message, '[ci skip]')\"\n     permissions:\n       security-events: write\n-    timeout-minutes: 30\n-    runs-on: ubuntu-20.04\n+    timeout-minutes: 15\n+    runs-on: ubuntu-22.04\n     steps:\n       - name: Checkout repository\n-        uses: actions/checkout@v3\n+        uses: actions/checkout@v4\n         with:\n           persist-credentials: false\n       - name: Initialize CodeQL\n-        uses: github/codeql-action/init@v2\n+        uses: github/codeql-action/init@v3\n         with:\n           languages: javascript, python\n           queries: +security-extended\n       - name: Perform CodeQL Analysis\n-        uses: github/codeql-action/analyze@v2\n+        uses: github/codeql-action/analyze@v3\n \n   irc:\n     timeout-minutes: 2\n     continue-on-error: true\n-    runs-on: ubuntu-20.04\n+    runs-on: ubuntu-22.04\n     needs: [linters, tests, tests-docker, codeql]\n     if: \"always() &amp;&amp; github.repository_owner == 'qutebrowser'\"\n     steps:\ndiff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml\nindex 91003fd08..9dc925e29 100644\n--- a/.github/workflows/docker.yml\n+++ b/.github/workflows/docker.yml\n@@ -10,31 +10,34 @@ jobs:\n     if: \"github.repository == 'qutebrowser/qutebrowser'\"\n     runs-on: ubuntu-20.04\n     strategy:\n+      fail-fast: false\n       matrix:\n         image:\n           - archlinux-webkit\n           - archlinux-webengine\n           - archlinux-webengine-unstable\n+          - archlinux-webengine-unstable-qt6\n+          - archlinux-webengine-qt6\n     steps:\n-      - uses: actions/checkout@v3\n-      - uses: actions/setup-python@v4\n+      - uses: actions/checkout@v4\n+      - uses: actions/setup-python@v5\n         with:\n           python-version: '3.x'\n       - run: pip install jinja2\n       - name: Generate Dockerfile\n         run: python3 generate.py ${{ matrix.image }}\n         working-directory: scripts/dev/ci/docker/\n-      - uses: docker/setup-buildx-action@v2\n-      - uses: docker/login-action@v2\n+      - uses: docker/setup-buildx-action@v3\n+      - uses: docker/login-action@v3\n         with:\n           username: qutebrowser\n           password: ${{ secrets.DOCKER_TOKEN }}\n-      - uses: docker/build-push-action@v3\n+      - uses: docker/build-push-action@v5\n         with:\n           file: scripts/dev/ci/docker/Dockerfile\n           context: .\n           tags: \"qutebrowser/ci:${{ matrix.image }}\"\n-          push: ${{ github.ref == 'refs/heads/master' }}\n+          push: ${{ github.ref == 'refs/heads/main' }}\n \n   irc:\n     timeout-minutes: 2\ndiff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml\nindex 073f2c69c..b326c2ad6 100644\n--- a/.github/workflows/nightly.yml\n+++ b/.github/workflows/nightly.yml\n@@ -15,75 +15,43 @@ jobs:\n       matrix:\n         include:\n           - os: macos-11\n-            branch: master\n-            toxenv: build-release\n-            name: macos\n+            toxenv: build-release-qt5\n+            name: qt5-macos\n           - os: windows-2019\n-            args: --64bit\n-            branch: master\n-            toxenv: build-release\n-            name: windows-64bit\n-          - os: windows-2019\n-            args: --32bit\n-            branch: master\n-            toxenv: build-release\n-            name: windows-32bit\n-\n+            toxenv: build-release-qt5\n+            name: qt5-windows\n           - os: macos-11\n             args: --debug\n-            branch: master\n-            toxenv: build-release\n-            name: macos-debug\n+            toxenv: build-release-qt5\n+            name: qt5-macos-debug\n           - os: windows-2019\n-            args: --64bit --debug\n-            branch: master\n+            args: --debug\n+            toxenv: build-release-qt5\n+            name: qt5-windows-debug\n+          - os: macos-11\n             toxenv: build-release\n-            name: windows-64bit-debug\n+            name: macos\n           - os: windows-2019\n-            args: --32bit --debug\n-            branch: master\n             toxenv: build-release\n-            name: windows-32bit-debug\n-\n-          - os: macos-11\n-            branch: qt6-v2\n-            toxenv: build-release-qt6\n-            name: qt6-macos\n-          - os: windows-2019\n-            args: --64bit\n-            branch: qt6-v2\n-            toxenv: build-release-qt6\n-            name: qt6-windows-64bit\n+            name: windows\n           - os: macos-11\n             args: --debug\n-            branch: qt6-v2\n-            toxenv: build-release-qt6\n-            name: qt6-macos-debug\n+            toxenv: build-release\n+            name: macos-debug\n           - os: windows-2019\n-            args: --64bit --debug\n-            branch: qt6-v2\n-            toxenv: build-release-qt6\n-            name: qt6-windows-64bit-debug\n+            args: --debug\n+            toxenv: build-release\n+            name: windows-debug\n     runs-on: \"${{ matrix.os }}\"\n-    timeout-minutes: 30\n+    timeout-minutes: 45\n     steps:\n-      - uses: actions/checkout@v3\n+      - uses: actions/checkout@v4\n         with:\n-          ref: \"${{ matrix.branch }}\"\n           persist-credentials: false\n       - name: Set up Python\n-        uses: actions/setup-python@v4\n+        uses: actions/setup-python@v5\n         with:\n           python-version: \"3.10\"\n-      - name: Get asciidoc\n-        uses: actions/checkout@v3\n-        with:\n-          repository: asciidoc-py/asciidoc-py\n-          ref: '9.x'\n-          path: asciidoc\n-          persist-credentials: false\n-      - name: Move asciidoc out of the repo\n-        run: mv asciidoc ..\n       - name: Install dependencies\n         run: |\n             python -m pip install -U pip\n@@ -91,16 +59,19 @@ jobs:\n       - name: Patch qutebrowser for debugging\n         if: \"contains(matrix.args, '--debug')\"\n         run: |\n-          sed -i '' '/.-d., .--debug.,/s/$/ default=True,/' qutebrowser/qutebrowser.py\n+          sed '/.-d., .--debug.,/s/$/ default=True,/' qutebrowser/qutebrowser.py &gt; qutebrowser/qutebrowser.py.tmp\n+          cp qutebrowser/qutebrowser.py.tmp qutebrowser/qutebrowser.py\n+          rm qutebrowser/qutebrowser.py.tmp\n       - name: Run tox\n-        run: \"tox -e ${{ matrix.toxenv }} -- --asciidoc ../asciidoc/asciidoc.py --gh-token ${{ secrets.GITHUB_TOKEN }} ${{ matrix.args }}\"\n+        run: \"tox -e ${{ matrix.toxenv }} -- --gh-token ${{ secrets.GITHUB_TOKEN }} ${{ matrix.args }}\"\n       - name: Gather info\n         id: info\n         run: |\n-            echo \"::set-output name=date::$(date +'%Y-%m-%d')\"\n-            echo \"::set-output name=sha_short::$(git rev-parse --short HEAD)\"\n+            echo \"date=$(date +'%Y-%m-%d')\" &gt;&gt; \"$GITHUB_OUTPUT\"\n+            echo \"sha_short=$(git rev-parse --short HEAD)\" &gt;&gt; \"$GITHUB_OUTPUT\"\n+        shell: bash\n       - name: Upload artifacts\n-        uses: actions/upload-artifact@v3\n+        uses: actions/upload-artifact@v4\n         with:\n           name: \"qutebrowser-nightly-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.name }}\"\n           path: |\ndiff --git a/.github/workflows/recompile-requirements.yml b/.github/workflows/recompile-requirements.yml\nindex 3c7442a61..6d42c3137 100644\n--- a/.github/workflows/recompile-requirements.yml\n+++ b/.github/workflows/recompile-requirements.yml\n@@ -18,15 +18,11 @@ jobs:\n     timeout-minutes: 20\n     runs-on: ubuntu-latest\n     steps:\n-      - uses: actions/checkout@v3\n+      - uses: actions/checkout@v4\n         with:\n           persist-credentials: false\n-      - name: Set up Python 3.7\n-        uses: actions/setup-python@v4\n-        with:\n-          python-version: '3.7'\n       - name: Set up Python 3.8\n-        uses: actions/setup-python@v4\n+        uses: actions/setup-python@v5\n         with:\n           python-version: '3.8'\n       - name: Recompile requirements\n@@ -35,7 +31,7 @@ jobs:\n       - name: Install apt dependencies\n         run: |\n             sudo apt-get update\n-            sudo apt-get install --no-install-recommends libyaml-dev libegl1-mesa libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 asciidoc python3-venv xvfb\n+            sudo apt-get install --no-install-recommends libyaml-dev libegl1-mesa libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0 asciidoc python3-venv xvfb\n       - name: Install dependencies\n         run: |\n             python -m pip install -U pip\n@@ -45,7 +41,7 @@ jobs:\n       - name: Run qutebrowser smoke test\n         run: \"xvfb-run .venv/bin/python3 -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ':later 500 quit'\"\n       - name: Create pull request\n-        uses: peter-evans/create-pull-request@v3\n+        uses: peter-evans/create-pull-request@v6\n         with:\n           committer: qutebrowser bot \n           author: qutebrowser bot \ndiff --git a/.github/workflows/release.yml b/.github/workflows/release.yml\nnew file mode 100644\nindex 000000000..aa8b3b2ef\n--- /dev/null\n+++ b/.github/workflows/release.yml\n@@ -0,0 +1,223 @@\n+name: Release\n+\n+on:\n+  workflow_dispatch:\n+    inputs:\n+      release_type:\n+        description: 'Release type'\n+        required: true\n+        default: 'patch'\n+        type: choice\n+        options:\n+          - 'patch'\n+          - 'minor'\n+          - 'major'\n+          # FIXME do we want a possibility to do prereleases here?\n+      python_version:\n+        description: 'Python version'\n+        required: true\n+        default: '3.12'\n+        type: choice\n+        options:\n+          - '3.8'\n+          - '3.9'\n+          - '3.10'\n+          - '3.11'\n+          - '3.12'\n+jobs:\n+  prepare:\n+    runs-on: ubuntu-20.04\n+    timeout-minutes: 5\n+    outputs:\n+      version: ${{ steps.bump.outputs.version }}\n+      release_id: ${{ steps.create-release.outputs.id }}\n+    permissions:\n+      contents: write  # To push release commit/tag\n+    steps:\n+      - name: Find release branch\n+        uses: actions/github-script@v7\n+        id: find-branch\n+        with:\n+          script: |\n+            if (context.payload.inputs.release_type != 'patch') {\n+              return 'main';\n+            }\n+            const branches = await github.paginate(github.rest.repos.listBranches, {\n+              owner: context.repo.owner,\n+              repo: context.repo.repo,\n+            });\n+            const branch_names = branches.map(branch =&gt; branch.name);\n+            console.log(`branches: ${branch_names}`);\n+            const release_branches = branch_names.filter(branch =&gt; branch.match(/^v\\d+\\.\\d+\\.x$/));\n+            if (release_branches.length === 0) {\n+              core.setFailed('No release branch found!');\n+              return '';\n+            }\n+            console.log(`release_branches: ${release_branches}`);\n+            // Get newest release branch (biggest version number)\n+            const sorted = release_branches.sort((a, b) =&gt; a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));\n+            console.log(`sorted: ${sorted}`);\n+            return sorted.at(-1);\n+          result-encoding: string\n+      - uses: actions/checkout@v4\n+      - name: Set up Python\n+        uses: actions/setup-python@v5\n+        with:\n+          # Doesn't really matter what we prepare the release with, but let's\n+          # use the same version for consistency.\n+          python-version: ${{ github.event.inputs.python_version }}\n+      - name: Install dependencies\n+        run: |\n+            python -m pip install -U pip\n+            python -m pip install -U -r misc/requirements/requirements-tox.txt\n+      - name: Configure git\n+        run: |\n+            git config --global user.name \"qutebrowser bot\"\n+            git config --global user.email \"bot@qutebrowser.org\"\n+      - name: Switch to release branch\n+        uses: actions/checkout@v4\n+        with:\n+          ref: ${{ steps.find-branch.outputs.result }}\n+      - name: Import GPG Key\n+        run: |\n+          gpg --import &lt;&lt;&lt; \"${{ secrets.QUTEBROWSER_BOT_GPGKEY }}\"\n+      - name: Bump version\n+        id: bump\n+        run: \"tox -e update-version -- ${{ github.event.inputs.release_type }}\"\n+      - name: Check milestone\n+        uses: actions/github-script@v7\n+        with:\n+          script: |\n+            const milestones = await github.paginate(github.rest.issues.listMilestones, {\n+              owner: context.repo.owner,\n+              repo: context.repo.repo,\n+            });\n+            const names = milestones.map(milestone =&gt; milestone.title);\n+            console.log(`milestones: ${names}`);\n+\n+            const milestone = milestones.find(milestone =&gt; milestone.title === \"v${{ steps.bump.outputs.version }}\");\n+            if (milestone !== undefined) {\n+              core.setFailed(`Found open milestone ${milestone.title} with ${milestone.open_issues} open and ${milestone.closed_issues} closed issues!`);\n+            }\n+      - name: Push release commit/tag\n+        run: |\n+            git push origin ${{ steps.find-branch.outputs.result }}\n+            git push origin v${{ steps.bump.outputs.version }}\n+      - name: Cherry-pick release commit\n+        if: ${{ github.event.inputs.release_type == 'patch' }}\n+        run: |\n+            git checkout main\n+            git cherry-pick -x v${{ steps.bump.outputs.version }}\n+            git push origin main\n+            git checkout v${{ steps.bump.outputs.version_x }}\n+      - name: Create release branch\n+        if: ${{ github.event.inputs.release_type != 'patch' }}\n+        run: |\n+            git checkout -b v${{ steps.bump.outputs.version_x }}\n+            git push --set-upstream origin v${{ steps.bump.outputs.version_x }}\n+      - name: Create GitHub draft release\n+        id: create-release\n+        uses: softprops/action-gh-release@v2\n+        with:\n+          tag_name: v${{ steps.bump.outputs.version }}\n+          draft: true\n+          body: \"*Release artifacts for this release are currently being uploaded...*\"\n+  release:\n+    strategy:\n+      matrix:\n+        include:\n+          - os: macos-11\n+          - os: windows-2019\n+          - os: ubuntu-20.04\n+    runs-on: \"${{ matrix.os }}\"\n+    timeout-minutes: 45\n+    needs: [prepare]\n+    permissions:\n+      contents: write  # To upload release artifacts\n+    steps:\n+      - uses: actions/checkout@v4\n+        with:\n+          ref: v${{ needs.prepare.outputs.version }}\n+      - name: Set up Python\n+        uses: actions/setup-python@v5\n+        with:\n+          python-version: ${{ github.event.inputs.python_version }}\n+      - name: Import GPG Key\n+        if: ${{ startsWith(matrix.os, 'ubuntu-') }}\n+        run: |\n+          gpg --import &lt;&lt;&lt; \"${{ secrets.QUTEBROWSER_BOT_GPGKEY }}\"\n+      # Needed because of the following import chain:\n+      # - scripts/dev/build_release.py\n+      # - scripts/dev/update_3rdparty.py\n+      # - scripts/dictcli.py\n+      # - qutebrowser/browser/webengine/spell.py\n+      # - utils.message -&gt; utils.usertypes -&gt; utils.qtutils -&gt; qt.gui\n+      # - PyQt6.QtGui\n+      # Some additional packages are needed for a2x to build manpage\n+      - name: Install apt dependencies\n+        if: ${{ startsWith(matrix.os, 'ubuntu-') }}\n+        run: |\n+            sudo apt-get update\n+            sudo apt-get install --no-install-recommends libegl1-mesa libxml2-utils docbook-xml xsltproc docbook-xsl\n+      - name: Install dependencies\n+        run: |\n+            python -m pip install -U pip\n+            python -m pip install -U -r misc/requirements/requirements-tox.txt\n+      # FIXME consider switching to trusted publishers:\n+      # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/\n+      - name: Build and upload release\n+        run: \"tox -e build-release -- --upload --no-confirm\"\n+        env:\n+          TWINE_USERNAME: __token__\n+          TWINE_PASSWORD: ${{ secrets.QUTEBROWSER_BOT_PYPI_TOKEN }}\n+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n+  finalize:\n+    runs-on: ubuntu-20.04\n+    timeout-minutes: 5\n+    needs: [prepare, release]\n+    permissions:\n+      contents: write  # To change release\n+    steps:\n+      - name: Publish final release\n+        uses: actions/github-script@v7\n+        with:\n+          script: |\n+            await github.rest.repos.updateRelease({\n+              owner: context.repo.owner,\n+              repo: context.repo.repo,\n+              release_id: \"${{ needs.prepare.outputs.release_id }}\",\n+              draft: false,\n+              body: \"Check the [changelog](https://github.com/qutebrowser/qutebrowser/blob/main/doc/changelog.asciidoc) for changes in this release.\",\n+            })\n+  irc:\n+    timeout-minutes: 2\n+    continue-on-error: true\n+    runs-on: ubuntu-20.04\n+    needs: [prepare, release, finalize]\n+    if: \"${{ always() }}\"\n+    steps:\n+      - name: Send success IRC notification\n+        uses: Gottox/irc-message-action@v2\n+        if: \"${{ needs.finalize.result == 'success' }}\"\n+        with:\n+          server: irc.libera.chat\n+          channel: '#qutebrowser-bots'\n+          nickname: qutebrowser-bot\n+          message: \"[${{ github.workflow }}] \\u00033Success:\\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})\"\n+      - name: Send main channel IRC notification\n+        uses: Gottox/irc-message-action@v2\n+        if: \"${{ needs.finalize.result == 'success' &amp;&amp; github.repository == 'qutebrowser/qutebrowser' }}\"\n+        with:\n+          server: irc.libera.chat\n+          channel: '#qutebrowser'\n+          nickname: qutebrowser-bot\n+          message: \"qutebrowser v${{ needs.prepare.outputs.version }} has been released! https://github.com/${{ github.repository }}/releases/tag/v${{ needs.prepare.outputs.version }}\"\n+      - name: Send non-success IRC notification\n+        uses: Gottox/irc-message-action@v2\n+        if: \"${{ needs.finalize.result != 'success' }}\"\n+        with:\n+          server: irc.libera.chat\n+          channel: '#qutebrowser-bots'\n+          nickname: qutebrowser-bot\n+          message: \"[${{ github.workflow }}] \\u00034FAIL:\\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})\\n\n+            prepare: ${{ needs.prepare.result }}, release: ${{ needs.release.result}}, finalize: ${{ needs.finalize.result }}\"\ndiff --git a/.mypy.ini b/.mypy.ini\nindex 820b2f966..81f69a09e 100644\n--- a/.mypy.ini\n+++ b/.mypy.ini\n@@ -1,16 +1,15 @@\n [mypy]\n-python_version = 3.7\n+python_version = 3.8\n \n ### --strict\n warn_unused_configs = True\n disallow_any_generics = True\n disallow_subclassing_any = True\n # disallow_untyped_calls = True\n-# disallow_untyped_defs = True\n+disallow_untyped_defs = True\n disallow_incomplete_defs = True\n check_untyped_defs = True\n disallow_untyped_decorators = True\n-# no_implicit_optional = True\n warn_redundant_casts = True\n warn_unused_ignores = True\n # warn_return_any = True\n@@ -23,22 +22,19 @@ disallow_any_unimported = True\n enable_error_code = ignore-without-code\n \n ### Output\n-show_error_codes = True\n show_error_context = True\n pretty = True\n \n-[mypy-colorama]\n-# https://github.com/tartley/colorama/issues/206\n-ignore_missing_imports = True\n+### FIXME:v4 get rid of this\n+no_implicit_optional = False\n+\n+### Future default behavior\n+local_partial_types = True\n \n [mypy-hunter]\n # https://github.com/ionelmc/python-hunter/issues/43\n ignore_missing_imports = True\n \n-[mypy-pygments.*]\n-# https://github.com/pygments/pygments/issues/1189\n-ignore_missing_imports = True\n-\n [mypy-objc]\n # https://github.com/ronaldoussoren/pyobjc/issues/417\n ignore_missing_imports = True\n@@ -47,87 +43,230 @@ ignore_missing_imports = True\n # https://github.com/ronaldoussoren/pyobjc/issues/417\n ignore_missing_imports = True\n \n-[mypy-qutebrowser.browser.browsertab]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.webkit.*]\n+ignore_errors = True\n \n-[mypy-qutebrowser.browser.hints]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.config.configtypes]\n+# Needs some major work to use specific generics\n+disallow_any_generics = False\n \n-[mypy-qutebrowser.browser.inspector]\n-disallow_untyped_defs = True\n+# Modules that are not fully typed yet\n+[mypy-qutebrowser.app]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.browser.webkit.webkitinspector]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.commands]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.browser.webengine.webengineinspector]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.downloads]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.browser.webengine.notification]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.downloadview]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.misc.guiprocess]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.eventfilter]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.misc.objects]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.greasemonkey]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.misc.quitter]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.history]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.misc.debugcachestats]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.navigate]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.misc.elf]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.network.pac]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.misc.utilcmds]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.network.proxy]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.misc.throttle]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.pdfjs]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.misc.backendproblem]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.qtnetworkdownloads]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.config.*]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.shared]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.config.configtypes]\n-# Needs some major work to use specific generics\n-disallow_any_generics = False\n+[mypy-qutebrowser.browser.signalfilter]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.api.*]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.urlmarks]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.components.*]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.webengine.cookies]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.extensions.*]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.webengine.interceptor]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.browser.webelem]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.webengine.spell]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.browser.webkit.webkitelem]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.webengine.tabhistory]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.browser.webengine.webengineelem]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.webengine.webenginedownloads]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.browser.webengine.darkmode]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.webengine.webenginequtescheme]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.keyinput.*]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.webengine.webenginesettings]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.utils.*]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.webengine.webenginetab]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.mainwindow.statusbar.command]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.browser.webengine.webview]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.browser.qutescheme]\n-disallow_untyped_defs = True\n+[mypy-qutebrowser.commands.argparser]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.commands.cmdexc]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.commands.command]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.commands.runners]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.commands.userscripts]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.completion.completer]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.completion.completiondelegate]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.completion.completionwidget]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.completion.models.completionmodel]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.completion.models.configmodel]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.completion.models.histcategory]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.completion.models.listcategory]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.completion.models.miscmodels]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.completion.models.urlmodel]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.completion.models.util]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.mainwindow]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.messageview]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.prompt]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.statusbar.backforward]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.statusbar.bar]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.statusbar.clock]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.statusbar.keystring]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.statusbar.percentage]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.statusbar.progress]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.statusbar.tabindex]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.statusbar.textbase]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.statusbar.url]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.tabbedbrowser]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.tabwidget]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.mainwindow.windowundo]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.autoupdate]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.checkpyver]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.cmdhistory]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.consolewidget]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.crashdialog]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.crashsignal]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.earlyinit]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.editor]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.httpclient]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.ipc]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.keyhintwidget]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.lineparser]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.miscwidgets]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.msgbox]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.pastebin]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.savemanager]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.sessions]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.misc.split]\n+disallow_untyped_defs = False\n+\n+[mypy-qutebrowser.qutebrowser]\n+disallow_untyped_defs = False\n \n-[mypy-qutebrowser.completion.models.filepathcategory]\n-disallow_untyped_defs = True\ndiff --git a/.pylintrc b/.pylintrc\nindex 47d3a163d..a6784c0e4 100644\n--- a/.pylintrc\n+++ b/.pylintrc\n@@ -1,12 +1,8 @@\n-# vim: ft=dosini fileencoding=utf-8:\n-\n [MASTER]\n ignore=resources.py\n-extension-pkg-whitelist=PyQt5,sip\n+extension-pkg-whitelist=PyQt5,PyQt6,sip\n load-plugins=qute_pylint.config,\n-             qute_pylint.modeline,\n              pylint.extensions.docstyle,\n-             pylint.extensions.emptystring,\n              pylint.extensions.overlapping_exceptions,\n              pylint.extensions.code_style,\n              pylint.extensions.comparison_placement,\n@@ -16,9 +12,11 @@ load-plugins=qute_pylint.config,\n              pylint.extensions.typing,\n              pylint.extensions.docparams,\n              pylint.extensions.private_import,\n+             pylint.extensions.dict_init_mutate,\n+             pylint.extensions.dunder\n \n persistent=n\n-py-version=3.7\n+py-version=3.8\n \n [MESSAGES CONTROL]\n enable=all\n@@ -50,6 +48,7 @@ disable=locally-disabled,\n         too-few-public-methods,\n         import-outside-toplevel,\n         consider-using-f-string,\n+        consider-using-assignment-expr,\n         logging-fstring-interpolation,\n         raise-missing-from,\n         consider-using-tuple,\n@@ -58,6 +57,9 @@ disable=locally-disabled,\n         missing-type-doc,\n         missing-param-doc,\n         useless-param-doc,\n+        wrong-import-order,  # doesn't work with qutebrowser.qt, even with known-third-party set\n+        ungrouped-imports,   # ditto\n+        use-implicit-booleaness-not-comparison-to-zero,\n \n [BASIC]\n function-rgx=[a-z_][a-z0-9_]{2,50}$\n@@ -72,8 +74,9 @@ no-docstring-rgx=(^_|^main$)\n class-const-naming-style = snake_case\n \n [FORMAT]\n-max-line-length=88\n-ignore-long-lines=( `cmd-set-text`\n+  * `repeat` -&gt; `cmd-repeat`\n+  * `repeat-command` -&gt; `cmd-repeat-last`\n+  * `later` -&gt; `cmd-later`\n+  * `edit-command` -&gt; `cmd-edit`\n+  * `run-with-count` -&gt; `cmd-run-with-count`\n+  The old names continue to work for the time being, but are deprecated and\n+  show a warning.\n+- Releases are now automated on CI, and GPG signed by\n+  `qutebrowser bot `, fingerprint\n+  `27F3 BB4F C217 EECB 8585  78AE EF7E E4D0 3969 0B7B`.\n+  The key is available as follows:\n+  * On https://qutebrowser.org/pubkey.gpg\n+  * Via keys.openpgp.org\n+  * Via WKD for bot@qutebrowser.org\n+- Support for old Qt versions (&lt; 5.15), old Python versions (&lt; 3.8) and old\n+  macOS (&lt; 11)/Windows (&lt; 10) versions were dropped. See the \"Removed\" section\n+  below for details.\n+\n Added\n ~~~~~\n \n@@ -27,7 +216,6 @@ Added\n - New `:prompt-fileselect-external` command which can be used to spawn an\n   external file selector (`fileselect.folder.command`) from download filename\n   prompts (bound to `` by default).\n-- New `clock` value for `statusbar.widgets`, displaying the current time.\n - New `qute://start` built-in start page (not set as the default start page yet).\n - New `content.javascript.log_message.levels` setting, allowing to surface JS log\n   messages as qutebrowser messages (rather than only logging them). By default,\n@@ -50,22 +238,43 @@ Added\n     * `qutedmenu` gained new `window` and `private` options.\n     * `qute-keepassxc` now supports unlock-on-demand, multiple account\n       selection via rofi, and inserting TOTP-codes (experimental).\n+    * `qute-pass` will now try looking up candidate pass entries based on the\n+      calling tab's verbatim netloc (hostname including port and username) if it\n+      can't find a match with an earlier candidate (FQDN, IPv4 etc).\n+- New `qt.chromium.experimental_web_platform_features` setting, which is enabled\n+  on Qt 5 by default, to maximize compatibility with websites despite an aging\n+  Chromium backend.\n+- New `colors.webpage.darkmode.increase_text_contrast` setting for Qt 6.3+\n+- New `fonts.tooltip`, `colors.tooltip.bg` and `colors.tooltip.fg` settings.\n+- New `log-qt-events` debug flag for `-D`\n+- New `--all` flags for `:bookmark-del` and `:quickmark-del` to delete all\n+  quickmarks/bookmarks.\n \n Removed\n ~~~~~~~\n \n-- Support for Python 3.6 is dropped, as it's been\n-  https://discuss.python.org/t/python-3-6-rides-into-the-sunset/12964[end-of-life upstream]\n-  since December 2021. Python 3.7.0 or newer is now required.\n-- It's planned to drop support for various legacy platforms and libraries which\n-  are unsupported upstream, such as:\n-  * Qt before 5.15 LTS (plus adding support for Qt 6.2+)\n-  * The QtWebKit backend\n-  * macOS 10.14 (via Homebrew)\n-  * 32-bit Windows (via Qt)\n-  * Windows 8 (via Qt)\n-  * Windows 10 before 1809 (via Qt)\n-  * Possibly other more minor dependency changes\n+- Python 3.8.0 or newer is now required.\n+  - Support for Python 3.6 and 3.7 is dropped, as they both reached\n+    their https://endoflife.date/python[end of life] in December 2021 and June\n+    2023, respectively.\n+- Support for Qt/PyQt before 5.15.0 and QtWebEngine before 5.15.2 are now\n+  dropped, as older Qt versions are\n+  https://endoflife.date/qt[end-of-life upstream] since mid/late 2020\n+  (5.13/5.14) and late 2021 (5.12 LTS).\n+- The `--enable-webengine-inspector` flag is now dropped. It used to be ignored\n+  but still accepted, to allow doing a `:restart` from versions older than v2.0.0.\n+  Thus, switching from v1.x.x directly to v3.0.0 via `:restart` will not be possible.\n+- Support for macOS 10.14 and 10.15 is now dropped, raising the minimum\n+  required macOS version to macOS 11 Big Sur.\n+  * Qt 6.4 was the latest version to support macOS 10.14 and 10.15.\n+  * It should be possible to build a custom .dmg with Qt 6.4, but this is\n+    unsupported and not recommended.\n+- Support for Windows 8 and for Windows 10 before 1607 is now dropped.\n+  * Support for older Windows 10 versions might still be present in Qt 6.0/6.1/6.2\n+  * Support for Windows 8.1 is still present in Qt 5.15\n+  * It should be possible to build a custom .exe with those versions, but this\n+    is unsupported and not recommended.\n+- Support for 32-bit Windows is now dropped.\n \n Changed\n ~~~~~~~\n@@ -104,6 +313,49 @@ Changed\n   the ones produced by `:download --mhtml`.\n - The \"... called unimplemented GM_...\" messages are now logged as info JS\n   messages instead of errors.\n+- For QtNetwork downloads (e.g. `:adblock-update`), various changes were done\n+  for how redirects work:\n+  - Insecure redirects (HTTPS -&gt; HTTP) now fail the download.\n+  - 20 redirects are now allowed before the download fails rather than only 10.\n+  - A redirect to the same URL will now fail the download with too many\n+    redirects instead of being ignored.\n+- When a download fails in a way it'd leave an empty file around, the empty\n+  file is now deleted.\n+- With Qt 6, setting `content.headers.referer` to `always` will act as if it\n+  was set to `same-domain`. The documentation is now updated to point that out.\n+- With QtWebEngine 5.15.5+, the load finished workaround was dropped, which\n+  should make certain operations happen when the page has started loading rather\n+  when it fully finished.\n+- `mkvenv.py` has a new `--pyqt-snapshot` flag, allowing to install certain packages\n+  from the https://www.riverbankcomputing.com/pypi/[Riverbank development snapshots server].\n+- When `QUTE_QTWEBENGINE_VERSION_OVERRIDE` is set, it now always wins, no matter how\n+  the version would otherwise have been determined. Note setting this value can break\n+  things (if set to a wrong value), and usually isn't needed.\n+- When qutebrowser is run with an older QtWebEngine version as on the previous\n+  launch, it now prints an error before starting (which causes the underlying\n+  Chromium to remove all browsing data such as cookies).\n+- The keys \"\" and \"\" are now named \"\"\n+  and \"\", respectively.\n+- The `tox.ini` now requires at least tox 3.20 (was tox 3.15 previously).\n+- `:config-diff` now has an `--include-hidden` flag, which also shows\n+  internally-set settings.\n+- Improved error messages when `:spawn` can't find an executable.\n+- When a process fails, the error message now suggests using `:process PID` with\n+  the correct PID (rather than always showing the latest process, which might not\n+  be the failing one)\n+- When a process got killed with `SIGTERM`, no error message is now displayed\n+  anymore (unless started with `:spawn --verbose`).\n+- When a process got killed by a signal, the signal name is now displayed in\n+  the message.\n+- The `js-string-replaceall` quirk is now removed from the default\n+  `content.site_specific_quirks.skip`, so that `String.replaceAll` is now\n+  polyfilled on QtWebEngine &lt; 5.15.3, hopefully improving website\n+  compaitibility.\n+- Hints are now displayed for elements setting an `aria-haspopup` attribute.\n+- qutebrowser now uses SPDX license identifiers in its files. Full support for\n+  the https://reuse.software/[REUSE specification] (license provided in a\n+  machine-readable way for every single file) is not done yet, but planned for\n+  a future release.\n \n Fixed\n ~~~~~\n@@ -116,11 +368,52 @@ Fixed\n   shown, qutebrowser used to only show one message. This is now only done when the\n   two messages are completely equivalent (text, level, etc.) instead of doing so\n   when only the text matches.\n+- The `progress` and `backforward` statusbar widgets now stay removed if you\n+  choose to remove them. Previously they would appear again on navigation.\n+- Rare crash when running userscripts with crashed renderer processes.\n+- Multiple rare crashes when quitting qutebrowser.\n+- The `asciidoc2html.py` script now correctly uses the virtualenv-installed\n+  asciidoc rather than requiring a system-wide installation.\n+- \"Package would be ignored\" deprecation warnings when running `setup.py`.\n+- ResourceWarning when using `:restart`.\n+- Crash when shutting down before fully initialized.\n+- Crash with some notification servers when the server is quitting.\n+- Crash when using QtWebKit with PAC and the file has an invalid encoding.\n+- Crash with the \"tiramisu\" notification server.\n+- Crash when the \"herbe\" notification presenter doesn't start correctly.\n+- Crash when no notification server is installed/available.\n+- Warning with recent versions of the \"deadd\" (aka \"linux notification center\") notification server.\n+- Crash when using `:print --pdf` with a directory where its parent directory\n+  did not exist.\n+- The `PyQt{5,6}.sip` version is now shown correctly in the `:version`/`--version`\n+  output. Previously that showed the version from the standalone `sip` module\n+  which was only set for PyQt5. (#7805)\n+- When a `config.py` calls `.redirect()` via a request interceptor (which is\n+  unsupported) and supplies an invalid redirect target URL, an exception is now\n+  raised for the `.redirect()` call instead of later inside qutebrowser.\n+- Crash when loading invalid history items from a session file.\n+\n+[[v2.5.4]]\n+v2.5.4 (2023-03-13)\n+-------------------\n+\n+Fixed\n+~~~~~\n+\n+- Support SQLite with DQS (double quoted string) compile time option turned\n+  off.\n \n [[v2.5.3]]\n-v2.5.3 (unreleased)\n+v2.5.3 (2023-02-17)\n -------------------\n \n+Added\n+~~~~~\n+\n+- New `array_at` quirk, polyfilling the\n+  https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at[`Array.at` method],\n+  which is needed by various websites, but only natively available with Qt 6.2.\n+\n Fixed\n ~~~~~\n \n@@ -135,6 +428,10 @@ Fixed\n - Wrong type handling when using `:config-{dict,list}-*` commands with a config\n   option with non-string values. The only affected option is `bindings.commands`,\n   which is probably rarely used with those commands.\n+- The `readability` userscript now correctly passes the source URL to\n+  Breadability, to make relative links work.\n+- Update `dictcli.py` to use the `main` branch, fixing a 404 error.\n+- Crash with some notification servers when the server did quit.\n - Minor documentation fixes\n \n [[v2.5.2]]\n@@ -590,13 +887,13 @@ Changed\n - When a shown message replaces an existing related one (e.g. for zoom levels),\n   the replacing now also works even if a different message was shown in between.\n - The `.redirect(...)` method on interceptors now supports an\n-  `ignore_unsupported=True` argument which supresses exceptions if a request could\n+  `ignore_unsupported=True` argument which suppresses exceptions if a request could\n   not be redirected. Note, however, that it is still not public API.\n - When the `--config-py` argument is used, no warning about a missing\n   `config.load_autoconfig` is shown anymore, as the argument is typically used\n   for temporarily testing a config.\n - The internal `_autosave` session used for crash recovery is now only saved\n-  once per minute, since saving it for every page load is a noticable performance\n+  once per minute, since saving it for every page load is a noticeable performance\n   issue.\n - The `readability-js` userscript now displays a small header with page\n   information.\n@@ -1024,7 +1321,7 @@ Changed\n - `config.py` files now are required to have either\n   `config.load_autoconfig(False)` (don't load `autoconfig.yml`) or\n   `config.load_autoconfig()` (do load `autoconfig.yml`) in them.\n-- Various host-blocking settings have been renamed to accomodate the new ABP-like\n+- Various host-blocking settings have been renamed to accommodate the new ABP-like\n   adblocker:\n   * `content.host_blocking.enabled` -&gt; `content.blocking.enabled` (controlling both blockers)\n   * `content.host_blocking.whitelist` -&gt; `content.blocking.whitelist` (controlling both blockers)\n@@ -1064,7 +1361,7 @@ Changed\n   It also has compatibility issues with various websites (GitHub, Twitch, Android\n   Developer documentation, YouTube, ...). Since no newer Debian Stable is released\n   at the time of writing, it's recommended to\n-  https://github.com/qutebrowser/qutebrowser/blob/master/doc/install.asciidoc#installing-qutebrowser-with-virtualenv[install qutebrowser in a virtualenv]\n+  https://github.com/qutebrowser/qutebrowser/blob/main/doc/install.asciidoc#installing-qutebrowser-with-virtualenv[install qutebrowser in a virtualenv]\n   with a newer version of Qt/PyQt.\n - New optional dependency on the Python `adblock` library (see above for details).\n - The (formerly optional) `cssutils` dependency is now removed. It was only\n@@ -1200,11 +1497,11 @@ Fixed\n ~~~~~\n \n - Setting the `content.headers.referer` setting to `same-domain` (the default)\n-  was supposed to truncate referers to only the host with QtWebEngine.\n+  was supposed to truncate referrers to only the host with QtWebEngine.\n   Unfortunately, this functionality broke in Qt 5.14. It works properly again\n   with this release, including a test so this won't happen again.\n - With QtWebEngine 5.15, setting the `content.headers.referer` setting to\n-  `never` did still send referers. This is now fixed as well.\n+  `never` did still send referrers. This is now fixed as well.\n - In v1.14.0, a regression was introduced, causing a crash when qutebrowser was\n   closed after opening a download with PDF.js. This is now fixed.\n - With Qt 5.12, the `Object.fromEntries` JavaScript API is unavailable (it was\n@@ -1221,7 +1518,7 @@ Fixed\n   conversion was shown. This is now fixed.\n - Ever since Qt 5.11, fetching more completion data when that data is loaded\n   lazily (such as with history) and the last visible item is selected was broken.\n-  The exact reason is currently unknown, but this release adds a tenative fix.\n+  The exact reason is currently unknown, but this release adds a tentative fix.\n - When PgUp/PgDown were used to go beyond the last visible item, the above issue\n   caused a crash, which is now also fixed.\n - As a workaround for an overzealous Microsoft Defender false-positive detecting\n@@ -1337,9 +1634,9 @@ Changed\n   which fixes issues with e.g. formulas on Wikipedia.\n - The `readability-js` userscript now adds some CSS to improve the reader mode\n   styling in various scenarios:\n-  * Images are now shrinked to the page width, similarly to what Firefox' reader\n+  * Images are now shrunk to the page width, similarly to what Firefox' reader\n     mode does.\n-  * Some images ore now displayed as block (rather than inline) which is what\n+  * Some images are now displayed as block (rather than inline) which is what\n     Firefox' reader mode does as well.\n   * Blockquotes are now styled more distinctively, again based on the Firefox\n     reader mode.\n@@ -3161,7 +3458,7 @@ Fixed\n - Fix workaround for black screens or crashes with Nvidia cards\n - Handle a filesystem going read-only gracefully\n - Fix crash when setting `fonts.monospace`\n-- Fix list options not being modifyable via `.append()` in `config.py`\n+- Fix list options not being modifiable via `.append()` in `config.py`\n - Mark the content.notifications setting as QtWebKit only correctly\n - Fix wrong rendering of keys like `` in the completion\n \n@@ -3197,7 +3494,7 @@ Major changes\n   * New dependency on the QtSql module and Qt sqlite support.\n   * New dependency on the https://www.attrs.org/[attrs] project (packaged as\n     `python-attr` in some distributions).\n-  * The depedency on PyOpenGL (when using QtWebEngine) got removed. Note\n+  * The dependency on PyOpenGL (when using QtWebEngine) got removed. Note\n     that PyQt5.QtOpenGL is still a dependency.\n   * PyQt5.QtOpenGL is now always required, even with QtWebKit.\n - The QtWebEngine backend is now used by default. Note this means that\n@@ -3215,7 +3512,7 @@ Major changes\n   completion is too slow on your machine, try setting it to a few 1000 items.\n - Up/Down now navigates through the command history instead of selecting\n   completion items. Either use Tab to cycle through the completion, or\n-  https://github.com/qutebrowser/qutebrowser/blob/master/doc/help/configuring.asciidoc#migrating-older-configurations[restore the old behavior].\n+  https://github.com/qutebrowser/qutebrowser/blob/main/doc/help/configuring.asciidoc#migrating-older-configurations[restore the old behavior].\n \n Added\n ~~~~~\n@@ -3388,7 +3685,7 @@ Fixed\n - Continuing a search after clearing it now works correctly.\n - The tabbar and completion should now be more consistently and correctly\n   styled with various system styles.\n-- Applying styiles in `qt5ct` now shouldn't crash anymore.\n+- Applying styles in `qt5ct` now shouldn't crash anymore.\n - The validation for colors in stylesheets is now less strict,\n   allowing for all valid Qt values.\n - `data:` URLs now aren't added to the history anymore.\n@@ -4418,7 +4715,7 @@ Changed\n     * Add `-s`/`--space` argument to `:set-cmd-text` (as `:set-cmd-text \"foo \"` will now set the literal text `\"foo \"`)\n - Ignore `;;` for splitting with some commands like `:bind`.\n - Add unbound (new) default keybindings to config. This also adds a new `` special command.\n-    * To unbind a command keybinding without binding it to a new key, you now have to bind it to `` or it'll be readded automatically.\n+    * To unbind a command keybinding without binding it to a new key, you now have to bind it to `` or it'll be re-added automatically.\n - If an SSL error is raised multiple times with the same error/certificate/host/scheme/port, the user is only asked once.\n - Jump to last instead of first item when pressing Shift-Tab the first time in the completion.\n - Add a fullscreen keybinding.\ndiff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc\nindex 70447d8c5..144117677 100644\n--- a/doc/contributing.asciidoc\n+++ b/doc/contributing.asciidoc\n@@ -84,9 +84,9 @@ If you prefer to send a patch to the mailinglist, you can generate a patch\n based on your changes like this:\n \n ----\n-git format-patch origin/master &lt;1&gt;\n+git format-patch origin/main &lt;1&gt;\n ----\n-&lt;1&gt; Replace `master` by the branch your work was based on, e.g.,\n+&lt;1&gt; Replace `main` by the branch your work was based on, e.g.,\n `origin/develop`.\n \n Running qutebrowser\n@@ -111,9 +111,9 @@ unittests and several linters/checkers.\n Currently, the following tox environments are available:\n \n * Tests using https://www.pytest.org[pytest]:\n-  - `py37`, `py38`, ...: Run pytest for python 3.7/3.8/... with the system-wide PyQt.\n-  - `py37-pyqt512`, ..., `py37-pyqt515`: Run pytest with the given PyQt version (`py37-*` also works).\n-  - `py37-pyqt515-cov`: Run with coverage support (other Python/PyQt versions work too).\n+  - `py38`, `py39`, ...: Run pytest for python 3.8/3.9/... with the system-wide PyQt.\n+  - `py38-pyqt515`, ..., `py38-pyqt65`: Run pytest with the given PyQt version (`py39-*` etc. also works).\n+  - `py38-pyqt515-cov`: Run with coverage support (other Python/PyQt versions work too).\n * `flake8`: Run various linting checks via https://pypi.python.org/pypi/flake8[flake8].\n * `vulture`: Run https://pypi.python.org/pypi/vulture[vulture] to find\n   unused code portions.\n@@ -128,6 +128,9 @@ Currently, the following tox environments are available:\n     - untracked git files\n     - VCS conflict markers\n     - common spelling mistakes\n+* http://mypy-lang.org/[mypy] for static type checking:\n+  - `mypy-pyqt5` run mypy with PyQt5 installed\n+  - `mypy-pyqt6` run mypy with PyQt6 installed\n \n The default test suite is run with `tox`; the list of default\n environments is obtained with `tox -l`.\n@@ -153,7 +156,7 @@ smallest scope which makes sense. Most of the time, this will be line scope.\n false-positives, let me know! I'm still tweaking the parameters.\n \n \n-Running Specific Tests\n+Running specific tests\n ~~~~~~~~~~~~~~~~~~~~~~\n \n While you are developing you often don't want to run the full test\n@@ -168,18 +171,27 @@ Examples:\n \n ----\n # run only pytest tests which failed in last run:\n-tox -e py37 -- --lf\n+tox -e py38 -- --lf\n \n # run only the end2end feature tests:\n-tox -e py37 -- tests/end2end/features\n+tox -e py38 -- tests/end2end/features\n \n # run everything with undo in the generated name, based on the scenario text\n-tox -e py37 -- tests/end2end/features/test_tabs_bdd.py -k undo\n+tox -e py38 -- tests/end2end/features/test_tabs_bdd.py -k undo\n \n # run coverage test for specific file (updates htmlcov/index.html)\n-tox -e py37-cov -- tests/unit/browser/test_webelem.py\n+tox -e py38-cov -- tests/unit/browser/test_webelem.py\n ----\n \n+Specifying the backend for tests\n+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+\n+Tests automatically pick the backend based on what they manage to import. If\n+you have both backends available and you would like the tests to be run with a\n+specific one you can set either of a) the environment variable QUTE_TESTS_BACKEND\n+, or b) the command line argument --qute-backend, to the desired backend\n+(webkit/webengine).\n+\n Profiling\n ~~~~~~~~~\n \n@@ -219,7 +231,8 @@ Useful websites\n \n Some resources which might be handy:\n \n-* https://doc.qt.io/qt-5/classes.html[The Qt5 reference]\n+* https://doc.qt.io/qt-6/classes.html[The Qt 6 reference]\n+* https://doc.qt.io/qt-5/classes.html[The Qt 5 reference]\n * https://docs.python.org/3/library/index.html[The Python reference]\n * https://httpbin.org/[httpbin, a test service for HTTP requests/responses]\n * https://requestbin.com/[RequestBin, a service to inspect HTTP requests]\n@@ -274,7 +287,7 @@ Other\n Languages] (https://www.rfc-editor.org/errata_search.php?rfc=5646[Errata])\n * https://www.w3.org/TR/CSS2/[Cascading Style Sheets Level 2 Revision 1 (CSS\n 2.1) Specification]\n-* https://doc.qt.io/qt-5/stylesheet-reference.html[Qt Style Sheets Reference]\n+* https://doc.qt.io/qt-6/stylesheet-reference.html[Qt Style Sheets Reference]\n * https://mimesniff.spec.whatwg.org/[MIME Sniffing Standard]\n * https://spec.whatwg.org/[WHATWG specifications]\n * https://www.w3.org/html/wg/drafts/html/master/Overview.html[HTML 5.1 Nightly]\n@@ -353,7 +366,7 @@ All objects can be printed by starting with the `--debug` flag and using the\n \n The registry is mainly used for &lt;&gt;, but it can\n also be useful in places where using Qt's\n-https://doc.qt.io/qt-5/signalsandslots.html[signals and slots] mechanism would\n+https://doc.qt.io/qt-6/signalsandslots.html[signals and slots] mechanism would\n be difficult.\n \n Logging\n@@ -562,35 +575,46 @@ Chrome URLs\n ~~~~~~~~~~~\n \n With the QtWebEngine backend, qutebrowser supports several chrome:// urls which\n-can be useful for debugging:\n+can be useful for debugging.\n \n-- chrome://accessibility/\n-- chrome://appcache-internals/\n-- chrome://blob-internals/\n-- chrome://conversion-internals/ (QtWebEngine 5.15.3+)\n-- chrome://crash/ (crashes the current renderer process!)\n+Info pages:\n+\n+- chrome://device-log/ (QtWebEngine &gt;= 6.3)\n - chrome://gpu/\n-- chrome://gpuclean/ (crashes the current renderer process!)\n-- chrome://gpucrash/ (crashes qutebrowser!)\n-- chrome://gpuhang/ (hangs qutebrowser!)\n+- chrome://sandbox/ (Linux only)\n+\n+Misc. / Debugging pages:\n+\n+- chrome://dino/\n - chrome://histograms/\n+- chrome://network-errors/\n+- chrome://tracing/ (QtWebEngine &gt;= 5.15.3)\n+- chrome://ukm/ (QtWebEngine &gt;= 5.15.3)\n+- chrome://user-actions/ (QtWebEngine &gt;= 5.15.3)\n+- chrome://webrtc-logs/ (QtWebEngine &gt;= 5.15.3)\n+\n+Internals pages:\n+\n+- chrome://accessibility/\n+- chrome://appcache-internals/ (QtWebEngine &lt; 6.4)\n+- chrome://attribution-internals/ (QtWebEngine &gt;= 6.4)\n+- chrome://blob-internals/\n+- chrome://conversion-internals/ (QtWebEngine &gt;= 5.15.3 and &lt; 6.4)\n - chrome://indexeddb-internals/\n-- chrome://kill/ (kills the current renderer process!)\n - chrome://media-internals/\n-- chrome://net-internals/ (QtWebEngine 5.15.4+)\n-- chrome://network-errors/\n-- chrome://ppapiflashcrash/\n-- chrome://ppapiflashhang/\n+- chrome://net-internals/ (QtWebEngine &gt;= 5.15.4)\n - chrome://process-internals/\n - chrome://quota-internals/\n-- chrome://sandbox/ (Linux only)\n - chrome://serviceworker-internals/\n-- chrome://taskscheduler-internals/ (removed in QtWebEngine 5.14)\n-- chrome://tracing/ (QtWebEngine 5.15.3+)\n-- chrome://ukm/ (QtWebEngine 5.15.3+)\n-- chrome://user-actions/ (QtWebEngine 5.15.3+)\n - chrome://webrtc-internals/\n-- chrome://webrtc-logs/ (QtWebEngine 5.15.3+)\n+\n+Crash/hang pages:\n+\n+- chrome://crash/ (crashes the current renderer process!)\n+- chrome://gpuclean/ (crashes the current renderer process!)\n+- chrome://gpucrash/ (crashes qutebrowser!)\n+- chrome://gpuhang/ (hangs qutebrowser!)\n+- chrome://kill/ (kills the current renderer process!)\n \n QtWebEngine internals\n ~~~~~~~~~~~~~~~~~~~~~\n@@ -600,11 +624,14 @@ This is mostly useful for qutebrowser maintainers to work around issues in Qt -\n The hierarchy of widgets when QtWebEngine is involved looks like this:\n \n - qutebrowser has a `WebEngineTab` object, which is its abstraction over QtWebKit/QtWebEngine.\n-- The `WebEngineTab` has a `_widget` attribute, which is the https://doc.qt.io/qt-5/qwebengineview.html[QWebEngineView]\n-- That view has a https://doc.qt.io/qt-5/qwebenginepage.html[QWebEnginePage] for everything which doesn't require rendering.\n-- The view also has a layout with exactly one element (which also is its `focusProxy()`)\n-- That element is the  https://code.qt.io/cgit/qt/qtwebengine.git/tree/src/webenginewidgets/render_widget_host_view_qt_delegate_widget.cpp[RenderWidgetHostViewQtDelegateWidget] (it inherits https://doc.qt.io/qt-5/qquickwidget.html[QQuickWidget]) - also often referred to as RWHV or RWHVQDW. It can be obtained via `sip.cast(tab._widget.focusProxy(), QQuickWidget)`.\n-- Calling `rootObject()` on that gives us the https://doc.qt.io/qt-5/qquickitem.html[QQuickItem] where Chromium renders into (?). With it, we can do things like `.setRotation(20)`.\n+- The `WebEngineTab` has a `_widget` attribute, which is the https://doc.qt.io/qt-6/qwebengineview.html[QWebEngineView]\n+- That view has a https://doc.qt.io/qt-6/qwebenginepage.html[QWebEnginePage] for everything which doesn't require rendering.\n+- The view also has a layout with exactly one element (which also is its `focusProxy()`).\n+    - Qt 5: That element is the https://code.qt.io/cgit/qt/qtwebengine.git/tree/src/webenginewidgets/render_widget_host_view_qt_delegate_widget.cpp?h=5.15[RenderWidgetHostViewQtDelegateWidget] (it inherits https://doc.qt.io/qt-6/qquickwidget.html[QQuickWidget]) - also often referred to as RWHV or RWHVQDW. \n+      It can be obtained via `sip.cast(tab._widget.focusProxy(), QQuickWidget)`.\n+    - Qt 6: That element is the https://code.qt.io/cgit/qt/qtwebengine.git/tree/src/webenginewidgets/api/qwebengineview.cpp[WebEngineQuickWidget] (it inherits https://doc.qt.io/qt-6/qquickwidget.html[QQuickWidget]).\n+      It can be obtained via `tab._widget.focusProxy()`.\n+- Calling `rootObject()` on that gives us the https://doc.qt.io/qt-6/qquickitem.html[QQuickItem] where Chromium renders into (?). With it, we can do things like `.setRotation(20)`.\n \n Style conventions\n -----------------\n@@ -662,7 +689,6 @@ Return:\n +\n * The layout of a module should be roughly like this:\n   - Shebang (`#!/usr/bin/python`, if needed)\n-  - vim-modeline (`# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et`)\n   - Copyright\n   - GPL boilerplate\n   - Module docstring\n@@ -676,6 +702,41 @@ Return:\n   - `__magic__` methods\n   - other methods\n   - overrides of Qt methods\n+* Type hinting: the qutebrowser codebase uses type hints liberally to enable\n+  static type checking and autocompletion in editors.\n+  - We use http://mypy-lang.org/[mypy] in CI jobs to perform static type\n+    checking.\n+  - Not all of the codebase is covered by type hints currently. We encourage\n+    including type hints on all new code and even adding type hints to\n+    existing code if you find yourself working on some that isn't already\n+    covered. There are some module specific rules in the mypy config file,\n+    `.mypy.ini`, to make type hints strictly required in some areas.\n+  - More often than not mypy is correct when it raises issues. But don't be\n+    afraid to add `# type: ignore[...]` statements or casts if you need to.\n+    As an optional part of the language not all type information from third\n+    parties is always correct. Mypy will raise a new issue if it spots an\n+    \"ignore\" statement which is no longer needed because the underlying\n+    issue has been resolved.\n+  - One area where we have to take particular care is in code that deals\n+    with differences between PyQt5 and PyQt6. We try to write most code in a\n+    way that will work with either backend but when you need to deal with\n+    differences you should use a pattern like:\n++\n+[source,python]\n+----\n+if machinery.IS_QT5:\n+    ... # do PyQt5 specific implementation\n+else:\n+    # PyQt6\n+    ... # do PyQt6 specific implementation\n+----\n++\n+then you have to https://mypy.readthedocs.io/en/latest/command_line.html#cmdoption-mypy-always-true[tell]\n+  mypy to treat `machinery.IS_QT5` as a constant value then run mypy twice to\n+  cover both branches. There are a handful of variables in\n+  `qutebrowser/qt/machinery.py` that mypy needs to know about. There are tox\n+  jobs (`mypy-pyqt5` and `mypy-pyqt6`) that take care of telling mypy to use\n+  them as constants.\n \n Checklists\n ----------\n@@ -708,18 +769,30 @@ qutebrowser release\n \n * Make sure there are no unstaged changes and the tests are green.\n * Make sure all issues with the related milestone are closed.\n+* Mark the https://github.com/qutebrowser/qutebrowser/milestones[milestone] as closed.\n * Consider updating the completions for `content.headers.user_agent` in `configdata.yml`.\n-* Minor release: Consider updating some files from master:\n+* Minor release: Consider updating some files from main:\n   - `misc/requirements/` and `requirements.txt`\n   - `scripts/`\n+* Update changelog in main branch and ensure the correct version number has `(unreleased)`\n+* If necessary: Update changelog in release branch from main.\n+\n+**Automatic release via GitHub Actions (starting with v3.0.0):**\n+\n+* Double check Python version in `.github/workflows/release.yml`\n+* Run the `release` workflow on the `main` branch, e.g. via `gh workflow run release -f release_type=major` (`release_type` can be `major`, `minor` or `patch`; you can also override `python_version`)\n+\n+**Manual release:**\n+\n * Make sure Python is up-to-date on build machines.\n-* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones as closed.\n-* Update changelog in master branch\n-* If necessary: Update changelog in release branch from master.\n * Run `./.venv/bin/python3 scripts/dev/update_version.py {major,minor,patch}`.\n * Run the printed instructions accordingly.\n+\n+**Post release:**\n+\n * Update `qutebrowser-git` PKGBUILD if dependencies/install changed.\n * Add unreleased future versions to changelog\n * Update IRC topic\n * Announce to qutebrowser and qutebrowser-announce mailinglist.\n * Post announcement mail to subreddit\n+* Post on the website formerly known as Twitter\ndiff --git a/doc/faq.asciidoc b/doc/faq.asciidoc\nindex 4b3596285..bd75d7d30 100644\n--- a/doc/faq.asciidoc\n+++ b/doc/faq.asciidoc\n@@ -158,7 +158,7 @@ It also works nicely with rapid hints:\n :bind ;M hint --rapid links spawn umpv {hint-url}\n ----\n \n-How do I use qutebrowser with mutt?::\n+How do I use qutebrowser with mutt/neomutt or other mail clients?::\n     For security reasons, local files without `.html` extensions aren't\n     rendered as HTML, see\n     https://bugs.chromium.org/p/chromium/issues/detail?id=777737[this Chromium issue]\n@@ -166,8 +166,29 @@ How do I use qutebrowser with mutt?::\n     extension:\n +\n ----\n-    text/html; qutebrowser %s; needsterminal; nametemplate=%s.html\n+text/html; qutebrowser %s; needsterminal; nametemplate=%s.html\n ----\n++\n+Note that you might want to add additional options to qutebrowser, so that it\n+runs as a separate instance configured to disable JavaScript and avoid network\n+requests, in order to avoid privacy leaks when reading mails. The easiest way\n+to do so is by specifying a non-existent proxy server, e.g.:\n++\n+----\n+qutebrowser --temp-basedir -s content.proxy http://localhost:666 -s content.dns_prefetch false -s content.javascript.enabled false %s\n+----\n++\n+With Qt 6, using something like:\n++\n+----\n+qutebrowser --temp-basedir -s content.dns_prefetch false -s content.javascript.enabled false %s\n+----\n++\n+should lead to a similar result, due to a more restrictive implementation of\n+the `content.local_content_can_access_remote_urls` setting (`false` by default\n+already). However, it's advised to use a page like\n+https://www.emailprivacytester.com/[Email Privacy Tester] to verify your\n+configuration.\n \n What is the difference between bookmarks and quickmarks?::\n     Bookmarks will always use the title of the website as their name, but with quickmarks\n@@ -234,7 +255,7 @@ Why does it take longer to open a URL in qutebrowser than in chromium?::\n     loaded until it is detected that there is an instance running\n     to which the URL is then passed. This takes some time.\n     One workaround is to use this\n-    https://github.com/qutebrowser/qutebrowser/blob/master/scripts/open_url_in_instance.sh[script]\n+    https://github.com/qutebrowser/qutebrowser/blob/main/scripts/open_url_in_instance.sh[script]\n     and place it in your $PATH with the name \"qutebrowser\". This\n     script passes the URL via a unix socket to qutebrowser (if its\n     running already) using socat which is much faster and starts a new\n@@ -355,7 +376,7 @@ There is a total of four possible approaches to get dark websites:\n \n How do I make copy to clipboard buttons work?::\n You can `:set content.javascript.clipboard access` to allow this globally (not\n-recommended!), or `:set -u some.domain content.javascript.clipboad access` if\n+recommended!), or `:set -u some.domain content.javascript.clipboard access` if\n you want to limit the setting to `some.domain`.\n \n \n@@ -409,7 +430,7 @@ allowing him to work part-time on qutebrowser. If you keep your donation level\n for long enough, you can get some qutebrowser stickers!\n \n Why GitHub Sponsors?::\n-    GitHub Sponsors is a crowdfundign platform nicely integrated with\n+    GitHub Sponsors is a crowdfunding platform nicely integrated with\n     qutebrowser's existing GitHub page and a better offering than alternatives such\n     as Patreon or Liberapay.\n +\ndiff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc\nindex 0ba7b15a2..4d1610970 100644\n--- a/doc/help/commands.asciidoc\n+++ b/doc/help/commands.asciidoc\n@@ -40,6 +40,12 @@ possible to run or bind multiple commands by separating them with `;;`.\n |&lt;&gt;|Clear all message notifications.\n |&lt;&gt;|Click the element matching the given filter.\n |&lt;&gt;|Close the current window.\n+|&lt;&gt;|Open an editor to modify the current command.\n+|&lt;&gt;|Execute a command after some time.\n+|&lt;&gt;|Repeat a given command.\n+|&lt;&gt;|Repeat the last executed command.\n+|&lt;&gt;|Run a command with the given count.\n+|&lt;&gt;|Preset the statusbar to some text.\n |&lt;&gt;|Set all settings back to their default.\n |&lt;&gt;|Cycle an option between multiple values.\n |&lt;&gt;|Add a key/value pair to a dictionary option.\n@@ -60,7 +66,6 @@ possible to run or bind multiple commands by separating them with `;;`.\n |&lt;&gt;|Open the last/[count]th download.\n |&lt;&gt;|Remove the last/[count]th download from the list.\n |&lt;&gt;|Retry the first failed/[count]th download.\n-|&lt;&gt;|Open an editor to modify the current command.\n |&lt;&gt;|Open an external editor with the currently selected form field.\n |&lt;&gt;|Navigate to a url formed in an external editor.\n |&lt;&gt;|Send a fake keypress or key string to the website or qutebrowser.\n@@ -75,7 +80,6 @@ possible to run or bind multiple commands by separating them with `;;`.\n |&lt;&gt;|Insert text at cursor position.\n |&lt;&gt;|Evaluate a JavaScript string.\n |&lt;&gt;|Jump to the mark named by `key`.\n-|&lt;&gt;|Execute a command after some time.\n |&lt;&gt;|Start or stop recording a macro.\n |&lt;&gt;|Run a recorded macro.\n |&lt;&gt;|Show an error message in the statusbar.\n@@ -94,11 +98,8 @@ possible to run or bind multiple commands by separating them with `;;`.\n |&lt;&gt;|Save the current page as a quickmark.\n |&lt;&gt;|Quit qutebrowser.\n |&lt;&gt;|Reload the current/[count]th tab.\n-|&lt;&gt;|Repeat a given command.\n-|&lt;&gt;|Repeat the last executed command.\n |&lt;&gt;|Report a bug in qutebrowser.\n |&lt;&gt;|Restart qutebrowser while keeping existing tabs open.\n-|&lt;&gt;|Run a command with the given count.\n |&lt;&gt;|Save configs and state.\n |&lt;&gt;|Take a screenshot of the currently shown part of the page.\n |&lt;&gt;|Scroll the current tab in the given direction.\n@@ -114,7 +115,6 @@ possible to run or bind multiple commands by separating them with `;;`.\n |&lt;&gt;|Load a session.\n |&lt;&gt;|Save a session.\n |&lt;&gt;|Set an option.\n-|&lt;&gt;|Preset the statusbar to some text.\n |&lt;&gt;|Set a mark at the current scroll position in the current tab.\n |&lt;&gt;|Spawn an external command.\n |&lt;&gt;|Stop loading in the current/[count]th tab.\n@@ -204,7 +204,7 @@ If no url and title are provided, then save the current page as a bookmark. If a\n \n [[bookmark-del]]\n === bookmark-del\n-Syntax: +:bookmark-del ['url']+\n+Syntax: +:bookmark-del [*--all*] ['url']+\n \n Delete a bookmark.\n \n@@ -212,6 +212,9 @@ Delete a bookmark.\n * +'url'+: The url of the bookmark to delete. If not given, use the current page's url.\n \n \n+==== optional arguments\n+* +*-a*+, +*--all*+: If given, delete all bookmarks.\n+\n ==== note\n * This command does not split arguments after the last argument and handles quotes literally.\n \n@@ -281,6 +284,96 @@ The given filter needs to result in exactly one element, otherwise, an error is\n === close\n Close the current window.\n \n+[[cmd-edit]]\n+=== cmd-edit\n+Syntax: +:cmd-edit [*--run*]+\n+\n+Open an editor to modify the current command.\n+\n+==== optional arguments\n+* +*-r*+, +*--run*+: Run the command if the editor exits successfully.\n+\n+[[cmd-later]]\n+=== cmd-later\n+Syntax: +:cmd-later 'duration' 'command'+\n+\n+Execute a command after some time.\n+\n+==== positional arguments\n+* +'duration'+: Duration to wait in format XhYmZs or a number for milliseconds.\n+* +'command'+: The command to run, with optional args.\n+\n+==== note\n+* This command does not split arguments after the last argument and handles quotes literally.\n+* With this command, +;;+ is interpreted literally instead of splitting off a second command.\n+* This command does not replace variables like +\\{url\\}+.\n+\n+[[cmd-repeat]]\n+=== cmd-repeat\n+Syntax: +:cmd-repeat 'times' 'command'+\n+\n+Repeat a given command.\n+\n+==== positional arguments\n+* +'times'+: How many times to repeat.\n+* +'command'+: The command to run, with optional args.\n+\n+==== count\n+Multiplies with 'times' when given.\n+\n+==== note\n+* This command does not split arguments after the last argument and handles quotes literally.\n+* With this command, +;;+ is interpreted literally instead of splitting off a second command.\n+* This command does not replace variables like +\\{url\\}+.\n+\n+[[cmd-repeat-last]]\n+=== cmd-repeat-last\n+Repeat the last executed command.\n+\n+==== count\n+Which count to pass the command.\n+\n+[[cmd-run-with-count]]\n+=== cmd-run-with-count\n+Syntax: +:cmd-run-with-count 'count-arg' 'command'+\n+\n+Run a command with the given count.\n+\n+If cmd_run_with_count itself is run with a count, it multiplies count_arg.\n+\n+==== positional arguments\n+* +'count-arg'+: The count to pass to the command.\n+* +'command'+: The command to run, with optional args.\n+\n+==== count\n+The count that run_with_count itself received.\n+\n+==== note\n+* This command does not split arguments after the last argument and handles quotes literally.\n+* With this command, +;;+ is interpreted literally instead of splitting off a second command.\n+* This command does not replace variables like +\\{url\\}+.\n+\n+[[cmd-set-text]]\n+=== cmd-set-text\n+Syntax: +:cmd-set-text [*--space*] [*--append*] [*--run-on-count*] 'text'+\n+\n+Preset the statusbar to some text.\n+\n+==== positional arguments\n+* +'text'+: The commandline to set.\n+\n+==== optional arguments\n+* +*-s*+, +*--space*+: If given, a space is added to the end.\n+* +*-a*+, +*--append*+: If given, the text is appended to the current text.\n+* +*-r*+, +*--run-on-count*+: If given with a count, the command is run with the given count rather than setting the command text.\n+\n+\n+==== count\n+The count if given.\n+\n+==== note\n+* This command does not split arguments after the last argument and handles quotes literally.\n+\n [[config-clear]]\n === config-clear\n Syntax: +:config-clear [*--save*]+\n@@ -337,8 +430,13 @@ Remove a key from a dict.\n \n [[config-diff]]\n === config-diff\n+Syntax: +:config-diff [*--include-hidden*]+\n+\n Show all customized options.\n \n+==== optional arguments\n+* +*-i*+, +*--include-hidden*+: Also include internal qutebrowser settings.\n+\n [[config-edit]]\n === config-edit\n Syntax: +:config-edit [*--no-source*]+\n@@ -507,15 +605,6 @@ Retry the first failed/[count]th download.\n ==== count\n The index of the download to retry.\n \n-[[edit-command]]\n-=== edit-command\n-Syntax: +:edit-command [*--run*]+\n-\n-Open an editor to modify the current command.\n-\n-==== optional arguments\n-* +*-r*+, +*--run*+: Run the command if the editor exits successfully.\n-\n [[edit-text]]\n === edit-text\n Open an external editor with the currently selected form field.\n@@ -775,21 +864,6 @@ Jump to the mark named by `key`.\n ==== positional arguments\n * +'key'+: mark identifier; capital indicates a global mark\n \n-[[later]]\n-=== later\n-Syntax: +:later 'duration' 'command'+\n-\n-Execute a command after some time.\n-\n-==== positional arguments\n-* +'duration'+: Duration to wait in format XhYmZs or a number for milliseconds.\n-* +'command'+: The command to run, with optional args.\n-\n-==== note\n-* This command does not split arguments after the last argument and handles quotes literally.\n-* With this command, +;;+ is interpreted literally instead of splitting off a second command.\n-* This command does not replace variables like +\\{url\\}+.\n-\n [[macro-record]]\n === macro-record\n Syntax: +:macro-record ['register']+\n@@ -821,7 +895,7 @@ Show an error message in the statusbar.\n * +'text'+: The text to show.\n \n ==== optional arguments\n-* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].\n+* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].\n \n \n [[message-info]]\n@@ -834,7 +908,7 @@ Show an info message in the statusbar.\n * +'text'+: The text to show.\n \n ==== optional arguments\n-* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].\n+* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].\n \n \n ==== count\n@@ -850,7 +924,7 @@ Show a warning message in the statusbar.\n * +'text'+: The text to show.\n \n ==== optional arguments\n-* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].\n+* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].\n \n \n [[messages]]\n@@ -993,7 +1067,7 @@ You can view all saved quickmarks on the link:qute://bookmarks[bookmarks page].\n \n [[quickmark-del]]\n === quickmark-del\n-Syntax: +:quickmark-del ['name']+\n+Syntax: +:quickmark-del [*--all*] ['name']+\n \n Delete a quickmark.\n \n@@ -1002,6 +1076,9 @@ Delete a quickmark.\n  if there are more than one).\n \n \n+==== optional arguments\n+* +*-a*+, +*--all*+: Delete all quickmarks.\n+\n ==== note\n * This command does not split arguments after the last argument and handles quotes literally.\n \n@@ -1051,31 +1128,6 @@ Reload the current/[count]th tab.\n ==== count\n The tab index to reload.\n \n-[[repeat]]\n-=== repeat\n-Syntax: +:repeat 'times' 'command'+\n-\n-Repeat a given command.\n-\n-==== positional arguments\n-* +'times'+: How many times to repeat.\n-* +'command'+: The command to run, with optional args.\n-\n-==== count\n-Multiplies with 'times' when given.\n-\n-==== note\n-* This command does not split arguments after the last argument and handles quotes literally.\n-* With this command, +;;+ is interpreted literally instead of splitting off a second command.\n-* This command does not replace variables like +\\{url\\}+.\n-\n-[[repeat-command]]\n-=== repeat-command\n-Repeat the last executed command.\n-\n-==== count\n-Which count to pass the command.\n-\n [[report]]\n === report\n Syntax: +:report ['info'] ['contact']+\n@@ -1091,26 +1143,6 @@ Report a bug in qutebrowser.\n === restart\n Restart qutebrowser while keeping existing tabs open.\n \n-[[run-with-count]]\n-=== run-with-count\n-Syntax: +:run-with-count 'count-arg' 'command'+\n-\n-Run a command with the given count.\n-\n-If run_with_count itself is run with a count, it multiplies count_arg.\n-\n-==== positional arguments\n-* +'count-arg'+: The count to pass to the command.\n-* +'command'+: The command to run, with optional args.\n-\n-==== count\n-The count that run_with_count itself received.\n-\n-==== note\n-* This command does not split arguments after the last argument and handles quotes literally.\n-* With this command, +;;+ is interpreted literally instead of splitting off a second command.\n-* This command does not replace variables like +\\{url\\}+.\n-\n [[save]]\n === save\n Syntax: +:save ['what' ...]+\n@@ -1142,7 +1174,7 @@ Syntax: +:scroll 'direction'+\n \n Scroll the current tab in the given direction.\n \n-Note you can use `:run-with-count` to have a keybinding with a bigger scroll increment.\n+Note you can use `:cmd-run-with-count` to have a keybinding with a bigger scroll increment.\n \n ==== positional arguments\n * +'direction'+: In which direction to scroll (up/down/left/right/top/bottom).\n@@ -1309,27 +1341,6 @@ If the option name ends with '?' or no value is provided, the value of the optio\n * +*-p*+, +*--print*+: Print the value after setting.\n * +*-u*+, +*--pattern*+: The link:configuring{outfilesuffix}#patterns[URL pattern] to use.\n \n-[[set-cmd-text]]\n-=== set-cmd-text\n-Syntax: +:set-cmd-text [*--space*] [*--append*] [*--run-on-count*] 'text'+\n-\n-Preset the statusbar to some text.\n-\n-==== positional arguments\n-* +'text'+: The commandline to set.\n-\n-==== optional arguments\n-* +*-s*+, +*--space*+: If given, a space is added to the end.\n-* +*-a*+, +*--append*+: If given, the text is appended to the current text.\n-* +*-r*+, +*--run-on-count*+: If given with a count, the command is run with the given count rather than setting the command text.\n-\n-\n-==== count\n-The count if given.\n-\n-==== note\n-* This command does not split arguments after the last argument and handles quotes literally.\n-\n [[set-mark]]\n === set-mark\n Syntax: +:set-mark 'key'+\n@@ -2176,7 +2187,7 @@ Syntax: +:debug-webaction 'action'+\n \n Execute a webaction.\n \n-Available actions: https://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit) https://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine)\n+Available actions: https://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit) https://doc.qt.io/qt-6/qwebenginepage.html#WebAction-enum (WebEngine)\n \n ==== positional arguments\n * +'action'+: The action to execute, e.g. MoveToNextChar.\ndiff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc\nindex 3ecef8ecf..d61743040 100644\n--- a/doc/help/configuring.asciidoc\n+++ b/doc/help/configuring.asciidoc\n@@ -413,6 +413,9 @@ Pre-built colorschemes\n - https://gitlab.com/lovetocode999/selenized-qutebrowser[Selenized]\n - https://github.com/The-Compiler/dotfiles/blob/master/qutebrowser/gruvbox.py[gruvbox]\n - https://www.opencode.net/wakellor957/qb-breath/-/blob/main/qb-breath.py[Manjaro Breath-like]\n+- https://github.com/gicrisf/qute-city-lights[City Lights (matte dark)]\n+- https://github.com/catppuccin/qutebrowser[Catppuccin]\n+- https://github.com/iruzo/matrix-qutebrowser[Matrix]\n \n Avoiding flake8 errors\n ^^^^^^^^^^^^^^^^^^^^^^\n@@ -449,6 +452,7 @@ Various emacs/conkeror-like keybinding configs exist:\n - https://gitlab.com/Kaligule/qutebrowser-emacs-config/blob/master/config.py[Kaligule]\n - https://web.archive.org/web/20210512185023/https://me0w.net/pit/1540882719[nm0i]\n - https://www.reddit.com/r/qutebrowser/comments/eh10i7/config_share_qute_with_emacs_keybindings/[jasonsun0310]\n+- https://git.sr.ht/~willvaughn/dots/tree/mjolnir/item/.config/qutebrowser/qutemacs.py[willvaughn]\n \n It's also mostly possible to get rid of modal keybindings by setting\n `input.insert_mode.auto_enter` to `false`, and `input.forward_unbound_keys` to\ndiff --git a/doc/help/index.asciidoc b/doc/help/index.asciidoc\nindex 127cc5d86..4f2b009c7 100644\n--- a/doc/help/index.asciidoc\n+++ b/doc/help/index.asciidoc\n@@ -1,3 +1,5 @@\n+// SPDX-License-Identifier: GPL-3.0-or-later\n+\n qutebrowser help\n ================\n \n@@ -6,7 +8,7 @@ Documentation\n \n The following help pages are currently available:\n \n-* link:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png[Key binding cheatsheet]\n+* link:https://raw.githubusercontent.com/qutebrowser/qutebrowser/main/doc/img/cheatsheet-big.png[Key binding cheatsheet]\n * link:../quickstart{outfilesuffix}[Quick start guide]\n * link:../faq{outfilesuffix}[Frequently asked questions]\n * link:../changelog{outfilesuffix}[Change Log]\ndiff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc\nindex 93c43cd8b..f2a5062c2 100644\n--- a/doc/help/settings.asciidoc\n+++ b/doc/help/settings.asciidoc\n@@ -112,16 +112,16 @@\n |&lt;&gt;|Foreground color of selected even tabs.\n |&lt;&gt;|Background color of selected odd tabs.\n |&lt;&gt;|Foreground color of selected odd tabs.\n+|&lt;&gt;|Background color of tooltips.\n+|&lt;&gt;|Foreground color of tooltips.\n |&lt;&gt;|Background color for webpages if unset (or empty to use the theme's color).\n-|&lt;&gt;|Which algorithm to use for modifying how colors are rendered with darkmode.\n+|&lt;&gt;|Which algorithm to use for modifying how colors are rendered with dark mode.\n |&lt;&gt;|Contrast for dark mode.\n |&lt;&gt;|Render all web contents using a dark theme.\n-|&lt;&gt;|Render all colors as grayscale.\n-|&lt;&gt;|Desaturation factor for images in dark mode.\n |&lt;&gt;|Which images to apply dark mode to.\n |&lt;&gt;|Which pages to apply dark mode to.\n |&lt;&gt;|Threshold for inverting background elements with dark mode.\n-|&lt;&gt;|Threshold for inverting text with dark mode.\n+|&lt;&gt;|Threshold for inverting text with dark mode.\n |&lt;&gt;|Value to use for `prefers-color-scheme:` for websites.\n |&lt;&gt;|Number of commands to save in the command history.\n |&lt;&gt;|Delay (in milliseconds) before updating completions after typing a character.\n@@ -171,6 +171,7 @@\n |&lt;&gt;|Allow JavaScript to open new tabs without user interaction.\n |&lt;&gt;|Allow JavaScript to read from or write to the clipboard.\n |&lt;&gt;|Enable JavaScript.\n+|&lt;&gt;|Enables the legacy touch event feature.\n |&lt;&gt;|Log levels to use for JavaScript console logging messages.\n |&lt;&gt;|Javascript messages to *not* show in the UI, despite a corresponding `content.javascript.log_message.levels` setting.\n |&lt;&gt;|Javascript message sources/levels to show in the qutebrowser UI.\n@@ -217,7 +218,7 @@\n |&lt;&gt;|Encoding to use for the editor.\n |&lt;&gt;|Delete the temporary file upon closing the editor.\n |&lt;&gt;|Command (and arguments) to use for selecting a single folder in forms. The command should write the selected folder path to the specified file or stdout.\n-|&lt;&gt;|Handler for selecting file(s) in forms. If `external`, then the commands specified by `fileselect.single_file.command` and `fileselect.multiple_files.command` are used to select one or multiple files respectively.\n+|&lt;&gt;|Handler for selecting file(s) in forms. If `external`, then the commands specified by `fileselect.single_file.command`, `fileselect.multiple_files.command` and `fileselect.folder.command` are used to select one file, multiple files, and folders, respectively.\n |&lt;&gt;|Command (and arguments) to use for selecting multiple files in forms. The command should write the selected file paths to the specified file or to stdout, separated by newlines.\n |&lt;&gt;|Command (and arguments) to use for selecting a single file in forms. The command should write the selected file path to the specified file or stdout.\n |&lt;&gt;|Font used in the completion categories.\n@@ -236,6 +237,7 @@\n |&lt;&gt;|Font used in the statusbar.\n |&lt;&gt;|Font used for selected tabs.\n |&lt;&gt;|Font used for unselected tabs.\n+|&lt;&gt;|Font used for tooltips.\n |&lt;&gt;|Font family for cursive fonts.\n |&lt;&gt;|Font family for fantasy fonts.\n |&lt;&gt;|Font family for fixed fonts.\n@@ -290,6 +292,7 @@\n |&lt;&gt;|Show a filebrowser in download prompts.\n |&lt;&gt;|Rounding radius (in pixels) for the edges of prompts.\n |&lt;&gt;|Additional arguments to pass to Qt, without leading `--`.\n+|&lt;&gt;|Enables Web Platform features that are in development.\n |&lt;&gt;|When to use Chromium's low-end device mode.\n |&lt;&gt;|Which Chromium process model to use.\n |&lt;&gt;|What sandboxing mechanisms in Chromium to use.\n@@ -298,6 +301,7 @@\n |&lt;&gt;|Force a Qt platformtheme to use.\n |&lt;&gt;|Force software rendering for QtWebEngine.\n |&lt;&gt;|Turn on Qt HighDPI scaling.\n+|&lt;&gt;|Disable accelerated 2d canvas to avoid graphical glitches.\n |&lt;&gt;|Work around locale parsing issues in QtWebEngine 5.15.3.\n |&lt;&gt;|Delete the QtWebEngine Service Worker directory on every start.\n |&lt;&gt;|When/how to show the scrollbar.\n@@ -560,9 +564,9 @@ Default:\n * +pass:[&#x27;]+: +pass:[mode-enter jump_mark]+\n * +pass:[+]+: +pass:[zoom-in]+\n * +pass:[-]+: +pass:[zoom-out]+\n-* +pass:[.]+: +pass:[repeat-command]+\n-* +pass:[/]+: +pass:[set-cmd-text /]+\n-* +pass:[:]+: +pass:[set-cmd-text :]+\n+* +pass:[.]+: +pass:[cmd-repeat-last]+\n+* +pass:[/]+: +pass:[cmd-set-text /]+\n+* +pass:[:]+: +pass:[cmd-set-text :]+\n * +pass:[;I]+: +pass:[hint images tab]+\n * +pass:[;O]+: +pass:[hint links fill :open -t -r {hint-url}]+\n * +pass:[;R]+: +pass:[hint --rapid links window]+\n@@ -618,9 +622,9 @@ Default:\n * +pass:[&lt;back&gt;]+: +pass:[back]+\n * +pass:[&lt;forward&gt;]+: +pass:[forward]+\n * +pass:[=]+: +pass:[zoom]+\n-* +pass:[?]+: +pass:[set-cmd-text ?]+\n+* +pass:[?]+: +pass:[cmd-set-text ?]+\n * +pass:[@]+: +pass:[macro-run]+\n-* +pass:[B]+: +pass:[set-cmd-text -s :quickmark-load -t]+\n+* +pass:[B]+: +pass:[cmd-set-text -s :quickmark-load -t]+\n * +pass:[D]+: +pass:[tab-close -o]+\n * +pass:[F]+: +pass:[hint all tab]+\n * +pass:[G]+: +pass:[scroll-to-perc]+\n@@ -630,7 +634,7 @@ Default:\n * +pass:[L]+: +pass:[forward]+\n * +pass:[M]+: +pass:[bookmark-add]+\n * +pass:[N]+: +pass:[search-prev]+\n-* +pass:[O]+: +pass:[set-cmd-text -s :open -t]+\n+* +pass:[O]+: +pass:[cmd-set-text -s :open -t]+\n * +pass:[PP]+: +pass:[open -t -- {primary}]+\n * +pass:[Pp]+: +pass:[open -t -- {clipboard}]+\n * +pass:[R]+: +pass:[reload -f]+\n@@ -638,7 +642,7 @@ Default:\n * +pass:[Sh]+: +pass:[history]+\n * +pass:[Sq]+: +pass:[bookmark-list]+\n * +pass:[Ss]+: +pass:[set]+\n-* +pass:[T]+: +pass:[set-cmd-text -sr :tab-focus]+\n+* +pass:[T]+: +pass:[cmd-set-text -sr :tab-focus]+\n * +pass:[U]+: +pass:[undo -w]+\n * +pass:[V]+: +pass:[mode-enter caret ;; selection-toggle --line]+\n * +pass:[ZQ]+: +pass:[quit]+\n@@ -647,30 +651,30 @@ Default:\n * +pass:[\\]\\]]+: +pass:[navigate next]+\n * +pass:[`]+: +pass:[mode-enter set_mark]+\n * +pass:[ad]+: +pass:[download-cancel]+\n-* +pass:[b]+: +pass:[set-cmd-text -s :quickmark-load]+\n+* +pass:[b]+: +pass:[cmd-set-text -s :quickmark-load]+\n * +pass:[cd]+: +pass:[download-clear]+\n * +pass:[co]+: +pass:[tab-only]+\n * +pass:[d]+: +pass:[tab-close]+\n * +pass:[f]+: +pass:[hint]+\n * +pass:[g$]+: +pass:[tab-focus -1]+\n * +pass:[g0]+: +pass:[tab-focus 1]+\n-* +pass:[gB]+: +pass:[set-cmd-text -s :bookmark-load -t]+\n+* +pass:[gB]+: +pass:[cmd-set-text -s :bookmark-load -t]+\n * +pass:[gC]+: +pass:[tab-clone]+\n * +pass:[gD]+: +pass:[tab-give]+\n * +pass:[gJ]+: +pass:[tab-move +]+\n * +pass:[gK]+: +pass:[tab-move -]+\n-* +pass:[gO]+: +pass:[set-cmd-text :open -t -r {url:pretty}]+\n+* +pass:[gO]+: +pass:[cmd-set-text :open -t -r {url:pretty}]+\n * +pass:[gU]+: +pass:[navigate up -t]+\n * +pass:[g^]+: +pass:[tab-focus 1]+\n * +pass:[ga]+: +pass:[open -t]+\n-* +pass:[gb]+: +pass:[set-cmd-text -s :bookmark-load]+\n+* +pass:[gb]+: +pass:[cmd-set-text -s :bookmark-load]+\n * +pass:[gd]+: +pass:[download]+\n * +pass:[gf]+: +pass:[view-source]+\n * +pass:[gg]+: +pass:[scroll-to-perc 0]+\n * +pass:[gi]+: +pass:[hint inputs --first]+\n * +pass:[gm]+: +pass:[tab-move]+\n-* +pass:[go]+: +pass:[set-cmd-text :open {url:pretty}]+\n-* +pass:[gt]+: +pass:[set-cmd-text -s :tab-select]+\n+* +pass:[go]+: +pass:[cmd-set-text :open {url:pretty}]+\n+* +pass:[gt]+: +pass:[cmd-set-text -s :tab-select]+\n * +pass:[gu]+: +pass:[navigate up]+\n * +pass:[h]+: +pass:[scroll left]+\n * +pass:[i]+: +pass:[mode-enter insert]+\n@@ -679,15 +683,15 @@ Default:\n * +pass:[l]+: +pass:[scroll right]+\n * +pass:[m]+: +pass:[quickmark-save]+\n * +pass:[n]+: +pass:[search-next]+\n-* +pass:[o]+: +pass:[set-cmd-text -s :open]+\n+* +pass:[o]+: +pass:[cmd-set-text -s :open]+\n * +pass:[pP]+: +pass:[open -- {primary}]+\n * +pass:[pp]+: +pass:[open -- {clipboard}]+\n * +pass:[q]+: +pass:[macro-record]+\n * +pass:[r]+: +pass:[reload]+\n * +pass:[sf]+: +pass:[save]+\n-* +pass:[sk]+: +pass:[set-cmd-text -s :bind]+\n-* +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+\n-* +pass:[ss]+: +pass:[set-cmd-text -s :set]+\n+* +pass:[sk]+: +pass:[cmd-set-text -s :bind]+\n+* +pass:[sl]+: +pass:[cmd-set-text -s :set -t]+\n+* +pass:[ss]+: +pass:[cmd-set-text -s :set]+\n * +pass:[tCH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.cookies.accept all no-3rdparty never ;; reload]+\n * +pass:[tCh]+: +pass:[config-cycle -p -u *://{url:host}/* content.cookies.accept all no-3rdparty never ;; reload]+\n * +pass:[tCu]+: +pass:[config-cycle -p -u {url} content.cookies.accept all no-3rdparty never ;; reload]+\n@@ -716,24 +720,24 @@ Default:\n * +pass:[tsu]+: +pass:[config-cycle -p -t -u {url} content.javascript.enabled ;; reload]+\n * +pass:[u]+: +pass:[undo]+\n * +pass:[v]+: +pass:[mode-enter caret]+\n-* +pass:[wB]+: +pass:[set-cmd-text -s :bookmark-load -w]+\n+* +pass:[wB]+: +pass:[cmd-set-text -s :bookmark-load -w]+\n * +pass:[wIf]+: +pass:[devtools-focus]+\n * +pass:[wIh]+: +pass:[devtools left]+\n * +pass:[wIj]+: +pass:[devtools bottom]+\n * +pass:[wIk]+: +pass:[devtools top]+\n * +pass:[wIl]+: +pass:[devtools right]+\n * +pass:[wIw]+: +pass:[devtools window]+\n-* +pass:[wO]+: +pass:[set-cmd-text :open -w {url:pretty}]+\n+* +pass:[wO]+: +pass:[cmd-set-text :open -w {url:pretty}]+\n * +pass:[wP]+: +pass:[open -w -- {primary}]+\n-* +pass:[wb]+: +pass:[set-cmd-text -s :quickmark-load -w]+\n+* +pass:[wb]+: +pass:[cmd-set-text -s :quickmark-load -w]+\n * +pass:[wf]+: +pass:[hint all window]+\n * +pass:[wh]+: +pass:[back -w]+\n * +pass:[wi]+: +pass:[devtools]+\n * +pass:[wl]+: +pass:[forward -w]+\n-* +pass:[wo]+: +pass:[set-cmd-text -s :open -w]+\n+* +pass:[wo]+: +pass:[cmd-set-text -s :open -w]+\n * +pass:[wp]+: +pass:[open -w -- {clipboard}]+\n-* +pass:[xO]+: +pass:[set-cmd-text :open -b -r {url:pretty}]+\n-* +pass:[xo]+: +pass:[set-cmd-text -s :open -b]+\n+* +pass:[xO]+: +pass:[cmd-set-text :open -b -r {url:pretty}]+\n+* +pass:[xo]+: +pass:[cmd-set-text -s :open -b]+\n * +pass:[yD]+: +pass:[yank domain -s]+\n * +pass:[yM]+: +pass:[yank inline [{title}\\]({url}) -s]+\n * +pass:[yP]+: +pass:[yank pretty-url -s]+\n@@ -1613,6 +1617,24 @@ Type: &lt;&gt;\n \n Default: +pass:[white]+\n \n+[[colors.tooltip.bg]]\n+=== colors.tooltip.bg\n+Background color of tooltips.\n+If set to null, the Qt default is used.\n+\n+Type: &lt;&gt;\n+\n+Default: empty\n+\n+[[colors.tooltip.fg]]\n+=== colors.tooltip.fg\n+Foreground color of tooltips.\n+If set to null, the Qt default is used.\n+\n+Type: &lt;&gt;\n+\n+Default: empty\n+\n [[colors.webpage.bg]]\n === colors.webpage.bg\n Background color for webpages if unset (or empty to use the theme's color).\n@@ -1623,7 +1645,7 @@ Default: +pass:[white]+\n \n [[colors.webpage.darkmode.algorithm]]\n === colors.webpage.darkmode.algorithm\n-Which algorithm to use for modifying how colors are rendered with darkmode.\n+Which algorithm to use for modifying how colors are rendered with dark mode.\n The `lightness-cielab` value was added with QtWebEngine 5.14 and is treated like `lightness-hsl` with older QtWebEngine versions.\n \n This setting requires a restart.\n@@ -1656,35 +1678,15 @@ Default: +pass:[0.0]+\n [[colors.webpage.darkmode.enabled]]\n === colors.webpage.darkmode.enabled\n Render all web contents using a dark theme.\n+On QtWebEngine &lt; 6.7, this setting requires a restart and does not support URL patterns, only the global setting is applied.\n Example configurations from Chromium's `chrome://flags`:\n-\n - \"With simple HSL/CIELAB/RGB-based inversion\": Set\n-  `colors.webpage.darkmode.algorithm` accordingly.\n+  `colors.webpage.darkmode.algorithm` accordingly, and\n+  set `colors.webpage.darkmode.policy.images` to `never`.\n \n-- \"With selective image inversion\": Set\n-  `colors.webpage.darkmode.policy.images` to `smart`.\n-\n-- \"With selective inversion of non-image elements\": Set\n-  `colors.webpage.darkmode.threshold.text` to 150 and\n-  `colors.webpage.darkmode.threshold.background` to 205.\n-\n-- \"With selective inversion of everything\": Combines the two variants\n-  above.\n-\n-This setting requires a restart.\n-\n-This setting is only available with the QtWebEngine backend.\n-\n-Type: &lt;&gt;\n-\n-Default: +pass:[false]+\n-\n-[[colors.webpage.darkmode.grayscale.all]]\n-=== colors.webpage.darkmode.grayscale.all\n-Render all colors as grayscale.\n-This only has an effect when `colors.webpage.darkmode.algorithm` is set to `lightness-hsl` or `brightness-rgb`.\n+- \"With selective image inversion\": qutebrowser default settings.\n \n-This setting requires a restart.\n+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].\n \n This setting is only available with the QtWebEngine backend.\n \n@@ -1692,25 +1694,9 @@ Type: &lt;&gt;\n \n Default: +pass:[false]+\n \n-[[colors.webpage.darkmode.grayscale.images]]\n-=== colors.webpage.darkmode.grayscale.images\n-Desaturation factor for images in dark mode.\n-If set to 0, images are left as-is. If set to 1, images are completely grayscale. Values between 0 and 1 desaturate the colors accordingly.\n-\n-This setting requires a restart.\n-\n-On QtWebEngine, this setting requires Qt 5.14 or newer.\n-\n-On QtWebKit, this setting is unavailable.\n-\n-Type: &lt;&gt;\n-\n-Default: +pass:[0.0]+\n-\n [[colors.webpage.darkmode.policy.images]]\n === colors.webpage.darkmode.policy.images\n Which images to apply dark mode to.\n-With QtWebEngine 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt].\n \n This setting requires a restart.\n \n@@ -1723,6 +1709,7 @@ Valid values:\n  * +always+: Apply dark mode filter to all images.\n  * +never+: Never apply dark mode filter to any images.\n  * +smart+: Apply dark mode based on image content. Not available with Qt 5.15.0.\n+ * +smart-simple+: On QtWebEngine 6.6, use a simpler algorithm for smart mode (based on numbers of colors and transparency), rather than an ML-based model. Same as 'smart' on older QtWebEnigne versions.\n \n Default: +pass:[smart]+\n \n@@ -1733,9 +1720,7 @@ The underlying Chromium setting has been removed in QtWebEngine 5.15.3, thus thi\n \n This setting requires a restart.\n \n-On QtWebEngine, this setting requires Qt 5.14 or newer.\n-\n-On QtWebKit, this setting is unavailable.\n+This setting is only available with the QtWebEngine backend.\n \n Type: &lt;&gt;\n \n@@ -1750,28 +1735,24 @@ Default: +pass:[smart]+\n === colors.webpage.darkmode.threshold.background\n Threshold for inverting background elements with dark mode.\n Background elements with brightness above this threshold will be inverted, and below it will be left as in the original, non-dark-mode page. Set to 256 to never invert the color or to 0 to always invert it.\n-Note: This behavior is the opposite of `colors.webpage.darkmode.threshold.text`!\n+Note: This behavior is the opposite of `colors.webpage.darkmode.threshold.foreground`!\n \n This setting requires a restart.\n \n-On QtWebEngine, this setting requires Qt 5.14 or newer.\n-\n-On QtWebKit, this setting is unavailable.\n+This setting is only available with the QtWebEngine backend.\n \n Type: &lt;&gt;\n \n Default: +pass:[0]+\n \n-[[colors.webpage.darkmode.threshold.text]]\n-=== colors.webpage.darkmode.threshold.text\n+[[colors.webpage.darkmode.threshold.foreground]]\n+=== colors.webpage.darkmode.threshold.foreground\n Threshold for inverting text with dark mode.\n Text colors with brightness below this threshold will be inverted, and above it will be left as in the original, non-dark-mode page. Set to 256 to always invert text color or to 0 to never invert text color.\n \n This setting requires a restart.\n \n-On QtWebEngine, this setting requires Qt 5.14 or newer.\n-\n-On QtWebKit, this setting is unavailable.\n+This setting is only available with the QtWebEngine backend.\n \n Type: &lt;&gt;\n \n@@ -1785,9 +1766,7 @@ The \"auto\" value is broken on QtWebEngine 5.15.2 due to a Qt bug. There, it will\n \n This setting requires a restart.\n \n-On QtWebEngine, this setting requires Qt 5.14 or newer.\n-\n-On QtWebKit, this setting is unavailable.\n+This setting is only available with the QtWebEngine backend.\n \n Type: &lt;&gt;\n \n@@ -2108,8 +2087,9 @@ Default: empty\n === content.canvas_reading\n Allow websites to read canvas elements.\n Note this is needed for some websites to work properly.\n+On QtWebEngine &lt; 6.6, this setting requires a restart and does not support URL patterns, only the global setting is applied.\n \n-This setting requires a restart.\n+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].\n \n This setting is only available with the QtWebEngine backend.\n \n@@ -2274,7 +2254,7 @@ Type: &lt;&gt;\n \n Valid values:\n \n- * +always+: Always send the Referer.\n+ * +always+: Always send the Referer. With QtWebEngine 6.2+, this value is unavailable and will act like `same-domain`.\n  * +never+: Never send the Referer. This is not recommended, as some sites may break.\n  * +same-domain+: Only send the Referer for the same domain. This will still protect your privacy, but shouldn't break any sites. With QtWebEngine, the referer will still be sent for other domains, but with stripped path information.\n \n@@ -2387,6 +2367,27 @@ Type: &lt;&gt;\n \n Default: +pass:[true]+\n \n+[[content.javascript.legacy_touch_events]]\n+=== content.javascript.legacy_touch_events\n+Enables the legacy touch event feature.\n+This affects JS APIs such as:\n+- ontouch* members on window, document, Element - document.createTouch, document.createTouchList - document.createEvent(\"TouchEvent\")\n+Newer Chromium versions have those disabled by default: https://bugs.chromium.org/p/chromium/issues/detail?id=392584 https://groups.google.com/a/chromium.org/g/blink-dev/c/KV6kqDJpYiE\n+\n+This setting requires a restart.\n+\n+This setting is only available with the QtWebEngine backend.\n+\n+Type: &lt;&gt;\n+\n+Valid values:\n+\n+ * +always+: Legacy touch events are always enabled. This might cause some websites to assume a mobile device.\n+ * +auto+: Legacy touch events are only enabled if a touch screen was detected on startup.\n+ * +never+: Legacy touch events are always disabled.\n+\n+Default: +pass:[never]+\n+\n [[content.javascript.log]]\n === content.javascript.log\n Log levels to use for JavaScript console logging messages.\n@@ -2415,13 +2416,13 @@ Default:\n \n - +pass:[userscript:_qute_stylesheet]+:\n \n-* +pass:[Refused to apply inline style because it violates the following Content Security Policy directive: *]+\n+* +pass:[*Refused to apply inline style because it violates the following Content Security Policy directive: *]+\n \n [[content.javascript.log_message.levels]]\n === content.javascript.log_message.levels\n Javascript message sources/levels to show in the qutebrowser UI.\n When a JavaScript message is logged from a location matching the glob pattern given in the key, and is from one of the levels listed as value, it's surfaced as a message in the qutebrowser UI.\n-By default, errors happening in qutebrowser internally or in userscripts are shown to the user.\n+By default, errors happening in qutebrowser internally are shown to the user.\n \n Type: &lt;&gt;\n \n@@ -2433,6 +2434,7 @@ Default:\n - +pass:[userscript:*]+:\n \n * +pass:[error]+\n+- +pass:[userscript:GM-*]+: empty\n \n [[content.javascript.modal_dialog]]\n === content.javascript.modal_dialog\n@@ -2578,8 +2580,6 @@ Allow websites to show notifications.\n \n This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].\n \n-On QtWebEngine, this setting requires Qt 5.13 or newer.\n-\n Type: &lt;&gt;\n \n Valid values:\n@@ -2594,8 +2594,6 @@ Default: +pass:[ask]+\n === content.notifications.presenter\n What notification presenter to use for web notifications.\n Note that not all implementations support all features of notifications:\n-- With PyQt 5.14, any setting other than `qt` does not support  the `click` and\n-  `close` events, as well as the `tag` option to replace existing notifications.\n - The `qt` and `systray` options only support showing one notification at the time\n   and ignore the `tag` option to replace existing notifications.\n - The `herbe` option only supports showing one notification at the time and doesn't\n@@ -2603,16 +2601,14 @@ Note that not all implementations support all features of notifications:\n - The `messages` option doesn't show icons and doesn't support the `click` and\n   `close` events.\n \n-On QtWebEngine, this setting requires Qt 5.14 or newer.\n-\n-On QtWebKit, this setting is unavailable.\n+This setting is only available with the QtWebEngine backend.\n \n Type: &lt;&gt;\n \n Valid values:\n \n  * +auto+: Tries `libnotify`, `systray` and `messages`, uses the first one available without showing error messages.\n- * +qt+: Use Qt's native notification presenter, based on a system tray icon. Switching from or to this value requires a restart of qutebrowser. Recommended over `systray` on PyQt 5.14.\n+ * +qt+: Use Qt's native notification presenter, based on a system tray icon. Switching from or to this value requires a restart of qutebrowser.\n  * +libnotify+: Shows messages via DBus in a libnotify-compatible way. If DBus isn't available, falls back to `systray` or `messages`, but shows an error message.\n  * +systray+: Use a notification presenter based on a systray icon. Falls back to `libnotify` or `messages` if not systray is available. This is a reimplementation of the `qt` setting value, but with the possibility to switch to it at runtime.\n  * +messages+: Show notifications as qutebrowser messages. Most notification features aren't available.\n@@ -2628,9 +2624,7 @@ Note that with the `qt` presenter, origins are never shown.\n \n This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].\n \n-On QtWebEngine, this setting requires Qt 5.14 or newer.\n-\n-On QtWebKit, this setting is unavailable.\n+This setting is only available with the QtWebEngine backend.\n \n Type: &lt;&gt;\n \n@@ -2683,9 +2677,7 @@ On Windows, if this setting is set to False, the system-wide animation setting i\n \n This setting requires a restart.\n \n-On QtWebEngine, this setting requires Qt 5.14 or newer.\n-\n-On QtWebKit, this setting is unavailable.\n+This setting is only available with the QtWebEngine backend.\n \n Type: &lt;&gt;\n \n@@ -2767,7 +2759,6 @@ Default: +pass:[true]+\n [[content.site_specific_quirks.skip]]\n === content.site_specific_quirks.skip\n Disable a list of named quirks.\n-The js-string-replaceall quirk is needed for Nextcloud Calendar &lt; 2.2.0 with QtWebEngine &lt; 5.15.3. However, the workaround is not fully compliant to the ECMAScript spec and might cause issues on other websites, so it's disabled by default.\n \n Type: &lt;&gt;\n \n@@ -2780,14 +2771,11 @@ Valid values:\n  * +js-whatsapp-web+\n  * +js-discord+\n  * +js-string-replaceall+\n- * +js-globalthis+\n- * +js-object-fromentries+\n+ * +js-array-at+\n  * +misc-krunker+\n  * +misc-mathml-darkmode+\n \n-Default: \n-\n-- +pass:[js-string-replaceall]+\n+Default: empty\n \n [[content.tls.certificate_errors]]\n === content.tls.certificate_errors\n@@ -3012,7 +3000,7 @@ Default:\n \n [[fileselect.handler]]\n === fileselect.handler\n-Handler for selecting file(s) in forms. If `external`, then the commands specified by `fileselect.single_file.command` and `fileselect.multiple_files.command` are used to select one or multiple files respectively.\n+Handler for selecting file(s) in forms. If `external`, then the commands specified by `fileselect.single_file.command`, `fileselect.multiple_files.command` and `fileselect.folder.command` are used to select one file, multiple files, and folders, respectively.\n \n Type: &lt;&gt;\n \n@@ -3188,6 +3176,15 @@ Type: &lt;&gt;\n \n Default: +pass:[default_size default_family]+\n \n+[[fonts.tooltip]]\n+=== fonts.tooltip\n+Font used for tooltips.\n+If set to null, the Qt default is used.\n+\n+Type: &lt;&gt;\n+\n+Default: empty\n+\n [[fonts.web.family.cursive]]\n === fonts.web.family.cursive\n Font family for cursive fonts.\n@@ -3480,10 +3477,12 @@ Default:\n * +pass:[[role=&quot;button&quot;\\]]+\n * +pass:[[role=&quot;tab&quot;\\]]+\n * +pass:[[role=&quot;checkbox&quot;\\]]+\n+* +pass:[[role=&quot;switch&quot;\\]]+\n * +pass:[[role=&quot;menuitem&quot;\\]]+\n * +pass:[[role=&quot;menuitemcheckbox&quot;\\]]+\n * +pass:[[role=&quot;menuitemradio&quot;\\]]+\n * +pass:[[role=&quot;treeitem&quot;\\]]+\n+* +pass:[[aria-haspopup\\]]+\n * +pass:[[ng-click\\]]+\n * +pass:[[ngClick\\]]+\n * +pass:[[data-ng-click\\]]+\n@@ -3633,9 +3632,7 @@ On Linux, disabling this also disables Chromium's MPRIS integration.\n \n This setting requires a restart.\n \n-On QtWebEngine, this setting requires Qt 5.14 or newer.\n-\n-On QtWebKit, this setting is unavailable.\n+This setting is only available with the QtWebEngine backend.\n \n Type: &lt;&gt;\n \n@@ -3825,6 +3822,25 @@ Type: &lt;&gt;\n \n Default: empty\n \n+[[qt.chromium.experimental_web_platform_features]]\n+=== qt.chromium.experimental_web_platform_features\n+Enables Web Platform features that are in development.\n+This passes the `--enable-experimental-web-platform-features` flag to Chromium. By default, this is enabled with Qt 5 to maximize compatibility despite an aging Chromium base.\n+\n+This setting requires a restart.\n+\n+This setting is only available with the QtWebEngine backend.\n+\n+Type: &lt;&gt;\n+\n+Valid values:\n+\n+ * +always+: Enable experimental web platform features.\n+ * +auto+: Enable experimental web platform features when using Qt 5.\n+ * +never+: Disable experimental web platform features.\n+\n+Default: +pass:[auto]+\n+\n [[qt.chromium.low_end_device_mode]]\n === qt.chromium.low_end_device_mode\n When to use Chromium's low-end device mode.\n@@ -3851,7 +3867,7 @@ Alternative process models use less resources, but decrease security and robustn\n See the following pages for more details:\n \n   - https://www.chromium.org/developers/design-documents/process-models\n-  - https://doc.qt.io/qt-5/qtwebengine-features.html#process-models\n+  - https://doc.qt.io/qt-6/qtwebengine-features.html#process-models\n \n This setting requires a restart.\n \n@@ -3948,7 +3964,7 @@ Default: +pass:[none]+\n [[qt.highdpi]]\n === qt.highdpi\n Turn on Qt HighDPI scaling.\n-This is equivalent to setting QT_AUTO_SCREEN_SCALE_FACTOR=1 or QT_ENABLE_HIGHDPI_SCALING=1 (Qt &gt;= 5.14) in the environment.\n+This is equivalent to setting QT_ENABLE_HIGHDPI_SCALING=1 (Qt &gt;= 5.14) in the environment.\n It's off by default as it can cause issues with some bitmap fonts. As an alternative to this, it's possible to set font sizes and the `zoom.default` setting.\n \n This setting requires a restart.\n@@ -3957,6 +3973,26 @@ Type: &lt;&gt;\n \n Default: +pass:[false]+\n \n+[[qt.workarounds.disable_accelerated_2d_canvas]]\n+=== qt.workarounds.disable_accelerated_2d_canvas\n+Disable accelerated 2d canvas to avoid graphical glitches.\n+On some setups graphical issues can occur on sites like Google sheets and PDF.js. These don't occur when accelerated 2d canvas is turned off, so we do that by default.\n+So far these glitches only occur on some Intel graphics devices.\n+\n+This setting requires a restart.\n+\n+This setting is only available with the QtWebEngine backend.\n+\n+Type: &lt;&gt;\n+\n+Valid values:\n+\n+ * +always+: Disable accelerated 2d canvas\n+ * +auto+: Disable on Qt6 &lt; 6.6.0, enable otherwise\n+ * +never+: Enable accelerated 2d canvas\n+\n+Default: +pass:[auto]+\n+\n [[qt.workarounds.locale]]\n === qt.workarounds.locale\n Work around locale parsing issues in QtWebEngine 5.15.3.\n@@ -4031,8 +4067,6 @@ Default: +pass:[true]+\n === search.wrap\n Wrap around at the top and bottom of the page when advancing through text matches using `:search-next` and `:search-prev`.\n \n-On QtWebEngine, this setting requires Qt 5.14 or newer.\n-\n Type: &lt;&gt;\n \n Default: +pass:[true]+\n@@ -4671,6 +4705,7 @@ Default:\n - +pass:[utm_campaign]+\n - +pass:[utm_term]+\n - +pass:[utm_content]+\n+- +pass:[utm_name]+\n \n [[window.hide_decoration]]\n === window.hide_decoration\n@@ -4807,7 +4842,7 @@ When setting from a string, pass a json-like list, e.g. `[\"one\", \"two\"]`.\n |Proxy|A proxy URL, or `system`/`none`.\n |QssColor|A color value supporting gradients.\n \n-A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#AARRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in https://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) * A gradient as explained in https://doc.qt.io/qt-5/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''\n+A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#AARRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in https://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) * A gradient as explained in https://doc.qt.io/qt-6/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''\n |QtColor|A color value.\n \n A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#AARRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in https://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)\ndiff --git a/doc/install.asciidoc b/doc/install.asciidoc\nindex 4b1d5bfff..98cc6fb05 100644\n--- a/doc/install.asciidoc\n+++ b/doc/install.asciidoc\n@@ -21,84 +21,61 @@ some distributions (notably, Debian Stable and Ubuntu) do only update\n qutebrowser and the underlying QtWebEngine when there's a new release of the\n distribution, typically once all couple of months to years.\n \n-On Debian / Ubuntu\n-------------------\n+[[debian]]\n+On Debian / Ubuntu / Linux Mint / ...\n+-------------------------------------\n \n-How to install qutebrowser depends a lot on the version of Debian/Ubuntu you're\n-running.\n-\n-[[ubuntu1604]]\n-Debian Stretch / Ubuntu 16.04 LTS / Linux Mint 18\n-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n-\n-Debian Stretch does have QtWebEngine packaged, but only in a very old and insecure\n-version (Qt 5.7, based on a Chromium from March 2016). Furthermore, it packages Python\n-3.5 which is not supported anymore since qutebrowser v2.0.0.\n-\n-Ubuntu 16.04 doesn't come with an up-to-date engine (a new enough QtWebKit, or\n-QtWebEngine) and also comes with Python 3.5.\n-\n-You should be able to install a newer Python (3.7+) using the\n-https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or\n-https://github.com/pyenv/pyenv[pyenv], and then proceed to\n-&lt;&gt;. However, this is currently untested. If you\n-got this setup to work successfully, please submit a pull request to adjust these\n-instructions!\n-\n-Note you'll need some basic libraries to use the virtualenv-installed PyQt:\n+With those distributions, qutebrowser is in the official repositories, and you\n+can install it with `apt install qutebrowser`.\n \n-----\n-# apt install --no-install-recommends git ca-certificates python3 python3-venv asciidoc libglib2.0-0 libgl1 libfontconfig1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xfixes0 libxcb-xinerama0 libxcb-xkb1 libxkbcommon-x11-0 libdbus-1-3 libyaml-dev gcc python3-dev libnss3 libasound2\n-----\n+However, when using a stable variant (e.g. Debian Stable / Ubuntu LTS / Linux\n+Mint), note that your versions of qutebrowser and the underlying QtWebEngine\n+will only be updated when there's a new release of your distribution (e.g.\n+upgrading from Ubuntu 22.04 to 24.04):\n \n-// FIXME not needed anymore?\n-// libxi6 libxrender1 libegl1-mesa\n+- Ubuntu 20.04, Linux Mint 20: qutebrowser 1.10.1, QtWebEngine 5.12.8 (based on Chromium 69 from 2018)\n+- Ubuntu 22.04, Linux Mint 21: qutebrowser 2.5.0, QtWebEngine 5.15.9 (based on Chromium 87 from 2020)\n+- Debian Bookworm: qutebrowser 2.5.3, QtWebEngine 5.15.13 (based on Chromium 87 from 2020)\n \n-Debian Buster / Ubuntu 18.04 LTS / Linux Mint 19\n-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+The old versions of the underlying Chromium will lead to various compatibility\n+issues. Additionally, QtWebEngine on all Debian-based distributions is\n+https://www.debian.org/releases/bookworm/amd64/release-notes/ch-information.en.html#browser-security[not covered]\n+by Debian's security support.\n \n-Debian Buster packages qutebrowser, but ships a very old version (v1.6.1 from March\n-2019). The QtWebEngine library used for rendering web contents is also very old (Qt\n-5.11, based on a Chromium from March 2018) and insecure. It is\n-https://www.debian.org/releases/buster/amd64/release-notes/ch-information.en.html#browser-security[not covered]\n-by Debian's security patches. It's recommended to &lt;&gt; with a newer PyQt/Qt binary instead.\n+It's recommended to &lt;&gt; with a newer PyQt/Qt binary instead.\n+If you need proprietary codec support or use an architecture not supported by Qt\n+binaries, starting with Ubuntu 22.04 and Debian Bookworm, it's possible to\n+install Qt 6 via apt. By using `mkvenv.py` with `--pyqt-type link` you get a\n+newer qutebrowser running with:\n \n-With Ubuntu 18.04, the situation looks similar (but worse): There, qutebrowser v1.1.1\n-from January 2018 is packaged, with QtWebEngine 5.9 based on a Chromium from January\n-2017. It's recommended to either upgrade to Ubuntu 20.04 LTS or &lt;&gt; with a newer PyQt/Qt binary instead.\n+- Ubuntu 22.04, Linux Mint 21: QtWebEngine 6.2.4 (based on Chromium 90 from mid-2021)\n+- Debian Bookworm: QtWebEngine 6.4.2 (based on Chromium 102 from mid-2022)\n \n Note you'll need some basic libraries to use the virtualenv-installed PyQt:\n \n ----\n-# apt install --no-install-recommends git ca-certificates python3 python3-venv asciidoc libglib2.0-0 libgl1 libfontconfig1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xfixes0 libxcb-xinerama0 libxcb-xkb1 libxkbcommon-x11-0 libdbus-1-3 libyaml-dev gcc python3-dev libnss3 libasound2\n-----\n-\n-Ubuntu 20.04 LTS / Linux Mint 20 (or newer)\n-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n-\n-With those distributions, qutebrowser is in the official repositories, and you\n-can install it with apt:\n-\n-----\n-# apt install qutebrowser\n+# apt install --no-install-recommends git ca-certificates python3 python3-venv libgl1 libxkbcommon-x11-0 libegl1-mesa libfontconfig1 libglib2.0-0 libdbus-1-3 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-shape0 libnss3 libxcomposite1 libxdamage1 libxrender1 libxrandr2 libxtst6 libxi6 libasound2\n ----\n \n Additional hints\n ~~~~~~~~~~~~~~~~\n \n+- On Ubuntu 20.04 / Linux Mint 20 / Debian Bullseye, no OpenSSL 3 is available.\n+  However, Qt 6.5 https://www.qt.io/blog/moving-to-openssl-3-in-binary-builds-starting-from-qt-6.5-beta-2[moved to OpenSSL 3]\n+  for its binary builds. Thus, you will either need to live with\n+  `:adblock-update` and `:download` being broken, or use `--pyqt-version 6.4` for\n+  the `mkvenv.py` script to get an older Qt.\n - If running from git, run the following to generate the documentation for the\n   `:help` command (the `mkvenv.py` script used with a virtualenv install already does\n   this for you):\n +\n ----\n-# apt install --no-install-recommends asciidoc\n+$ pip install -r misc/requirements/requirements-docs.txt  # or install asciidoc manually\n $ python3 scripts/asciidoc2html.py\n ----\n \n - If you prefer using QtWebKit, there's QtWebKit 5.212 available in\n-  Ubuntu 18.04 / Debian Buster or newer.  Note however that it is based on an upstream\n+  those distributions. Note however that it is based on an upstream\n   WebKit from September 2016 with known security issues and no sandboxing or process\n   isolation.\n - If video or sound don't work with QtWebKit, try installing the gstreamer plugins:\n@@ -130,6 +107,8 @@ For more information see https://rpmfusion.org/Configuration.\n # dnf install qt5-qtwebengine-freeworld\n -----\n \n+It's currently unknown what the Qt 6 equivalent of this is.\n+\n On Archlinux\n ------------\n \n@@ -292,7 +271,7 @@ Nightly builds\n \n If you want to test out new features before an official qutebrowser release, automated\n https://github.com/qutebrowser/qutebrowser/actions/workflows/nightly.yml[nightly\n-builds] are available. To download them, open the lastest run (usually the first one),\n+builds] are available. To download them, open the latest run (usually the first one),\n then download the archive at the bottom of the page.\n \n Those builds also include variants with debug logging enabled, which can be useful to\n@@ -301,14 +280,14 @@ track down issues.\n NOTE: Due to GitHub limitations, you need to be signed in with a GitHub account\n to download the files.\n \n-https://chocolatey.org/packages/qutebrowser[Chocolatey package]\n-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+Package managers\n+~~~~~~~~~~~~~~~~\n \n * PackageManagement PowerShell module\n ----\n PS C:\\&gt; Install-Package qutebrowser\n ----\n-* Chocolatey's client\n+* https://chocolatey.org/packages/qutebrowser[Chocolatey package] with `choco`:\n ----\n C:\\&gt; choco install qutebrowser\n ----\n@@ -369,7 +348,7 @@ Nightly builds\n \n If you want to test out new features before an official qutebrowser release, automated\n https://github.com/qutebrowser/qutebrowser/actions/workflows/nightly.yml[nightly\n-builds] are available. To download them, open the lastest run (usually the first one),\n+builds] are available. To download them, open the latest run (usually the first one),\n then download the archive at the bottom of the page.\n \n Those builds also include variants with debug logging enabled, which can be useful to\n@@ -388,26 +367,15 @@ qutebrowser from source.\n ==== Homebrew\n \n ----\n-$ brew install qt\n-(build PyQt and PyQtWebEngine from source)\n+$ brew install pyqt@6\n $ pip3 install qutebrowser\n ----\n \n-NOTE: Homebrew does not package PyQtWebEngine (Python wrappers for\n-QtWebEngine), so you will need to build that from sources manually.\n-\n-Since the v1.0 release, qutebrowser uses QtWebEngine by default.\n-\n-Homebrew's builds of Qt and PyQt don't come with QtWebKit (and `--with-qtwebkit`\n-uses an old version of QtWebKit which qutebrowser doesn't support anymore). If\n-you want QtWebKit support, you'll need to build an up-to-date QtWebKit\n-https://github.com/annulen/webkit/wiki/Building-QtWebKit-on-OS-X[manually].\n-\n Packagers\n ---------\n \n qutebrowser ships with a\n-https://github.com/qutebrowser/qutebrowser/blob/master/misc/Makefile[Makefile]\n+https://github.com/qutebrowser/qutebrowser/blob/main/misc/Makefile[Makefile]\n intended for packagers. This installs system-wide files in a proper locations,\n so it should be preferred to the usual `setup.py install` or `pip install`\n invocation.\n@@ -447,7 +415,7 @@ Installing dependencies (including Qt)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n \n Using a Qt installed via virtualenv needs a couple of system-wide libraries.\n-See the &lt;&gt; for details about which libraries\n+See the &lt;&gt; for details about which libraries\n are required.\n \n Then run the install script:\n@@ -463,7 +431,7 @@ This installs all needed Python dependencies in a `.venv` subfolder\n This comes with an up-to-date Qt/PyQt including a pre-compiled QtWebEngine\n binary, but has a few caveats:\n \n-- Make sure your `python3` is Python 3.7 or newer, otherwise you'll get a \"No\n+- Make sure your `python3` is Python 3.8 or newer, otherwise you'll get a \"No\n   matching distribution found\" error and/or qutebrowser will not run.\n - It only works on 64-bit x86 systems, with other architectures you'll get the\n   same error.\n@@ -514,18 +482,6 @@ You can create a simple wrapper script to start qutebrowser somewhere in your\n ~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser \"$@\"\n ----\n \n-Building the docs\n-~~~~~~~~~~~~~~~~~\n-\n-To build the documentation, install `asciidoc` (note that LaTeX which comes as\n-optional/recommended dependency with some distributions is not required).\n-\n-Then, run:\n-\n-----\n-$ python3 scripts/asciidoc2html.py\n-----\n-\n Updating\n ~~~~~~~~\n \ndiff --git a/doc/quickstart.asciidoc b/doc/quickstart.asciidoc\nindex b3e552aa5..733ef7ad4 100644\n--- a/doc/quickstart.asciidoc\n+++ b/doc/quickstart.asciidoc\n@@ -23,9 +23,9 @@ Basic keybindings to get you started\n What to do now\n --------------\n \n-* View the link:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png[key binding cheatsheet]\n+* View the link:https://raw.githubusercontent.com/qutebrowser/qutebrowser/main/doc/img/cheatsheet-big.png[key binding cheatsheet]\n to make yourself familiar with the key bindings: +\n-image:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-small.png[\"qutebrowser key binding cheatsheet\",link=\"https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png\"]\n+image:https://raw.githubusercontent.com/qutebrowser/qutebrowser/main/doc/img/cheatsheet-small.png[\"qutebrowser key binding cheatsheet\",link=\"https://raw.githubusercontent.com/qutebrowser/qutebrowser/main/doc/img/cheatsheet-big.png\"]\n * There's also a https://www.shortcutfoo.com/app/dojos/qutebrowser[free training\n   course] on shortcutfoo for the keybindings - note that you need to be in\n   insert mode (i) for it to work.\n@@ -62,10 +62,12 @@ qutebrowser's primary maintainer, The-Compiler, is currently working part-time o\n qutebrowser, funded by donations.\n \n To sustain this for a long time, your help is needed! Check the\n-https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] for more information.\n-Depending on your sign-up date and how long you keep a certain level, you can get\n-qutebrowser t-shirts, stickers and more!\n+https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] or\n+https://github.com/qutebrowser/qutebrowser/blob/main/README.asciidoc#donating[alternative\n+donation methods] for more information.  Depending on your sign-up date and how\n+long you keep a certain level, you can get qutebrowser t-shirts, stickers and\n+more!\n \n Alternatively, there are also various options available for one-time donations, see the\n-https://github.com/qutebrowser/qutebrowser/blob/master/README.asciidoc#donating[donation section]\n+https://github.com/qutebrowser/qutebrowser/blob/main/README.asciidoc#donating[donation section]\n in the README for details.\ndiff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc\nindex e83a4da0b..c96f7673f 100644\n--- a/doc/qutebrowser.1.asciidoc\n+++ b/doc/qutebrowser.1.asciidoc\n@@ -1,3 +1,5 @@\n+// SPDX-License-Identifier: GPL-3.0-or-later\n+\n // Note some sections in this file (everything between QUTE_*_START and\n // QUTE_*_END) are autogenerated by scripts/src2asciidoc.sh. DO NOT edit them\n // by hand.\n@@ -62,8 +64,11 @@ show it.\n *--backend* '{webkit,webengine}'::\n     Which backend to use.\n \n+*--qt-wrapper* '{PyQt6,PyQt5}'::\n+    Which Qt wrapper to use. This can also be set via the QUTE_QT_WRAPPER environment variable. If both are set, the command line argument takes precedence.\n+\n *--desktop-file-name* 'DESKTOP_FILE_NAME'::\n-    Set the base name of the desktop entry for this application. Used to set the app_id under Wayland. See https://doc.qt.io/qt-5/qguiapplication.html#desktopFileName-prop\n+    Set the base name of the desktop entry for this application. Used to set the app_id under Wayland. See https://doc.qt.io/qt-6/qguiapplication.html#desktopFileName-prop\n \n *--untrusted-args*::\n     Mark all following arguments as untrusted, which enforces that they are URLs/search terms (and not flags or commands)\n@@ -133,8 +138,14 @@ If you prefer, you can also write to the\n https://listi.jpberlin.de/mailman/listinfo/qutebrowser[mailinglist] at\n mailto:qutebrowser@lists.qutebrowser.org[] instead.\n \n-For security bugs, please contact me directly at me@the-compiler.org, GPG ID\n-https://www.the-compiler.org/pubkey.asc[0xFD55A072].\n+For security bugs, please contact security@qutebrowser.org (or if GPG\n+encryption is desired, contact me@the-compiler.org with GPG ID\n+https://www.the-compiler.org/pubkey.asc[0x916EB0C8FD55A072]).\n+\n+Alternatively,\n+https://github.com/qutebrowser/qutebrowser/security/advisories/new[report a vulnerability]\n+via GitHub's\n+https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability[private reporting feature].\n \n == COPYRIGHT\n This program is free software: you can redistribute it and/or modify it under\ndiff --git a/misc/Makefile b/misc/Makefile\nindex 62294ba61..39a7e005f 100644\n--- a/misc/Makefile\n+++ b/misc/Makefile\n@@ -4,6 +4,7 @@ ICONSIZES = 16 24 32 48 64 128 256 512\n DATAROOTDIR = $(PREFIX)/share\n DATADIR ?= $(DATAROOTDIR)\n MANDIR ?= $(DATAROOTDIR)/man\n+A2X ?= a2x\n \n ifdef DESTDIR\n SETUPTOOLSOPTS = --root=\"$(DESTDIR)\"\n@@ -14,7 +15,7 @@ all: man\n \n man: doc/qutebrowser.1\n doc/qutebrowser.1: doc/qutebrowser.1.asciidoc\n-\ta2x -f manpage $&lt;\n+\t$(A2X) -f manpage $&lt;\n \n install: man\n \t$(PYTHON) setup.py install --prefix=\"$(PREFIX)\" --optimize=1 $(SETUPTOOLSOPTS)\ndiff --git a/misc/nsis/install.nsh b/misc/nsis/install.nsh\nindex 8233ab5f0..5086dcb0d 100755\n--- a/misc/nsis/install.nsh\n+++ b/misc/nsis/install.nsh\n@@ -1,19 +1,6 @@\n-# Copyright 2018 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # NSIS installer header. Uses NsisMultiUser plugin and contains portions of\n # its demo code, copyright 2017 Richard Drizin, Alex Mitev.\n@@ -150,7 +137,7 @@ var KeepReg\n ; Functions\n Function CheckInstallation\n   ; if there's an installed version, uninstall it first (I chose not to start the uninstaller silently, so that user sees what failed)\n-  ; if both per-user and per-machine versions are installed, unistall the one that matches $MultiUser.InstallMode\n+  ; if both per-user and per-machine versions are installed, uninstall the one that matches $MultiUser.InstallMode\n   StrCpy $0 \"\"\n   ${if} $HasCurrentModeInstallation = 1\n     StrCpy $0 \"$MultiUser.InstallMode\"\n@@ -443,8 +430,37 @@ SectionEnd\n ; Callbacks\n Function .onInit\n   StrCpy $KeepReg 1\n-  !insertmacro CheckPlatform ${PLATFORM}\n-  !insertmacro CheckMinWinVer ${MIN_WIN_VER}\n+\n+; OS version check\n+  ${If} ${RunningX64}\n+    ; https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoa#remarks\n+    GetWinVer $R0 Major\n+    !if \"${QT5}\" == \"True\"\n+      IntCmpU $R0 6 0 _os_check_fail _os_check_pass\n+      GetWinVer $R1 Minor\n+      IntCmpU $R1 2 _os_check_pass _os_check_fail _os_check_pass\n+    !else\n+      IntCmpU $R0 10 0 _os_check_fail _os_check_pass\n+      GetWinVer $R1 Build\n+      ${If} $R1 &gt;= 22000 ; Windows 11 21H2\n+        Goto _os_check_pass\n+      ${ElseIf} $R1 &gt;= 14393 ; Windows 10 1607\n+      ${AndIf} ${IsNativeAMD64} ; Windows 10 has no x86_64 emulation on arm64\n+        Goto _os_check_pass\n+      ${EndIf}\n+    !endif\n+  ${EndIf}\n+  _os_check_fail:\n+  !if \"${QT5}\" == \"True\"\n+    MessageBox MB_OK|MB_ICONSTOP \"This version of ${PRODUCT_NAME} requires a 64-bit$\\r$\\n\\\n+      version of Windows 8 or later.\"\n+  !else\n+    MessageBox MB_OK|MB_ICONSTOP \"This version of ${PRODUCT_NAME} requires a 64-bit$\\r$\\n\\\n+      version of Windows 10 1607 or later.\"\n+  !endif\n+  Abort\n+  _os_check_pass:\n+\n   ${ifnot} ${UAC_IsInnerInstance}\n     !insertmacro CheckSingleInstance \"Setup\" \"Global\" \"${SETUP_MUTEX}\"\n     !insertmacro CheckSingleInstance \"Application\" \"Local\" \"${APP_MUTEX}\"\ndiff --git a/misc/nsis/install_pages.nsh b/misc/nsis/install_pages.nsh\nindex c3cf973df..ecea32fe5 100755\n--- a/misc/nsis/install_pages.nsh\n+++ b/misc/nsis/install_pages.nsh\n@@ -1,19 +1,6 @@\n-# Copyright 2018 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # NSIS pages header. Uses NsisMultiUser plugin and contains portions of\n # its demo code, copyright 2017 Richard Drizin, Alex Mitev.\ndiff --git a/misc/nsis/qutebrowser.nsi b/misc/nsis/qutebrowser.nsi\nindex 7623d8cb2..bd5156e83 100755\n--- a/misc/nsis/qutebrowser.nsi\n+++ b/misc/nsis/qutebrowser.nsi\n@@ -1,20 +1,7 @@\n-# Copyright 2018 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n # encoding: iso-8859-1\n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # NSIS installer script. Uses NsisMultiUser plugin and contains portions of\n # its demo code, copyright 2017 Richard Drizin, Alex Mitev.\n@@ -137,13 +124,16 @@ ShowUninstDetails hide\n \n ; If not defined, get VERSION from PROGEXE. Set DIST_DIR accordingly.\n !ifndef VERSION\n-  !define /ifndef DIST_DIR \".\\..\\..\\dist\\${PRODUCT_NAME}-${ARCH}\"\n+  !define /ifndef DIST_DIR \".\\..\\..\\dist\\${PRODUCT_NAME}\"\n   !getdllversion \"${DIST_DIR}\\${PROGEXE}\" expv_\n   !define VERSION \"${expv_1}.${expv_2}.${expv_3}\"\n !else\n-  !define /ifndef DIST_DIR \".\\..\\..\\dist\\${PRODUCT_NAME}-${VERSION}-${ARCH}\"\n+  !define /ifndef DIST_DIR \".\\..\\..\\dist\\${PRODUCT_NAME}-${VERSION}\"\n !endif\n \n+; If not defined, assume Qt6 (requires a more recent windows version)\n+!define /ifndef QT5 \"False\"\n+\n ; Pack the exe header with upx if UPX is defined.\n !ifdef UPX\n   !packhdr \"$%TEMP%\\exehead.tmp\" '\"upx\" \"--ultra-brute\" \"$%TEMP%\\exehead.tmp\"'\ndiff --git a/misc/nsis/uninstall.nsh b/misc/nsis/uninstall.nsh\nindex fc33b7aee..55bad591b 100755\n--- a/misc/nsis/uninstall.nsh\n+++ b/misc/nsis/uninstall.nsh\n@@ -1,19 +1,6 @@\n-# Copyright 2018 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # NSIS uninstaller header. Uses NsisMultiUser plugin and contains portions of\n # its demo code, copyright 2017 Richard Drizin, Alex Mitev.\ndiff --git a/misc/nsis/uninstall_pages.nsh b/misc/nsis/uninstall_pages.nsh\nindex 10bc30484..5548db3ed 100755\n--- a/misc/nsis/uninstall_pages.nsh\n+++ b/misc/nsis/uninstall_pages.nsh\n@@ -1,19 +1,6 @@\n-# Copyright 2018 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # NSIS pages header. Uses NsisMultiUser plugin and contains portions of\n # its demo code, copyright 2017 Richard Drizin, Alex Mitev.\ndiff --git a/misc/org.qutebrowser.qutebrowser.appdata.xml b/misc/org.qutebrowser.qutebrowser.appdata.xml\nindex 4f2880dc1..017303345 100644\n--- a/misc/org.qutebrowser.qutebrowser.appdata.xml\n+++ b/misc/org.qutebrowser.qutebrowser.appdata.xml\n@@ -1,5 +1,5 @@\n \n-\n+\n \n \torg.qutebrowser.qutebrowser\n \tCC-BY-SA-3.0\n@@ -23,16 +23,16 @@\n \torg.qutebrowser.qutebrowser.desktop\n \t\n \t\t\n-\t\t\thttps://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/main.png\n+\t\t\thttps://raw.githubusercontent.com/qutebrowser/qutebrowser/main/doc/img/main.png\n \t\t\n \t\t\n-\t\t\thttps://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/downloads.png\n+\t\t\thttps://raw.githubusercontent.com/qutebrowser/qutebrowser/main/doc/img/downloads.png\n \t\t\n \t\t\n-\t\t\thttps://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/completion.png\n+\t\t\thttps://raw.githubusercontent.com/qutebrowser/qutebrowser/main/doc/img/completion.png\n \t\t\n \t\t\n-\t\t\thttps://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/hints.png\n+\t\t\thttps://raw.githubusercontent.com/qutebrowser/qutebrowser/main/doc/img/hints.png\n \t\t\n \t\n \thttps://www.qutebrowser.org\n@@ -44,6 +44,12 @@\n \t\n \t\n \n+\n+\n+\n+\n+\n+\n \n \n \ndiff --git a/misc/qutebrowser.spec b/misc/qutebrowser.spec\nindex e12e9a9b0..652f69bfb 100644\n--- a/misc/qutebrowser.spec\n+++ b/misc/qutebrowser.spec\n@@ -64,17 +64,17 @@ INFO_PLIST_UPDATES = {\n \n def get_data_files():\n     data_files = [\n-        ('../qutebrowser/html', 'html'),\n-        ('../qutebrowser/img', 'img'),\n-        ('../qutebrowser/icons', 'icons'),\n-        ('../qutebrowser/javascript', 'javascript'),\n-        ('../qutebrowser/html/doc', 'html/doc'),\n-        ('../qutebrowser/git-commit-id', '.'),\n-        ('../qutebrowser/config/configdata.yml', 'config'),\n+        ('../qutebrowser/html', 'qutebrowser/html'),\n+        ('../qutebrowser/img', 'qutebrowser/img'),\n+        ('../qutebrowser/icons', 'qutebrowser/icons'),\n+        ('../qutebrowser/javascript', 'qutebrowser/javascript'),\n+        ('../qutebrowser/html/doc', 'qutebrowser/html/doc'),\n+        ('../qutebrowser/git-commit-id', 'qutebrowser/git-commit-id'),\n+        ('../qutebrowser/config/configdata.yml', 'qutebrowser/config'),\n     ]\n \n     if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')):\n-        data_files.append(('../qutebrowser/3rdparty/pdfjs', '3rdparty/pdfjs'))\n+        data_files.append(('../qutebrowser/3rdparty/pdfjs', 'qutebrowser/3rdparty/pdfjs'))\n     else:\n         print(\"Warning: excluding pdfjs as it's not present!\")\n \n@@ -82,7 +82,7 @@ def get_data_files():\n \n \n def get_hidden_imports():\n-    imports = ['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0']\n+    imports = [\"PyQt5.QtOpenGL\"] if \"PYINSTALLER_QT5\" in os.environ else []\n     for info in loader.walk_components():\n         imports.append(info.name)\n     return imports\n@@ -100,6 +100,11 @@ else:\n \n \n DEBUG = os.environ.get('PYINSTALLER_DEBUG', '').lower() in ['1', 'true']\n+if DEBUG:\n+  options = options = [('v', None, 'OPTION')]\n+else:\n+  options = []\n+\n \n \n a = Analysis(['../qutebrowser/__main__.py'],\n@@ -117,6 +122,7 @@ pyz = PYZ(a.pure, a.zipped_data,\n              cipher=block_cipher)\n exe = EXE(pyz,\n           a.scripts,\n+          options,\n           exclude_binaries=True,\n           name='qutebrowser',\n           icon=icon,\n@@ -137,5 +143,4 @@ app = BUNDLE(coll,\n              name='qutebrowser.app',\n              icon=icon,\n              info_plist=INFO_PLIST_UPDATES,\n-             # https://github.com/pyinstaller/pyinstaller/blob/b78bfe530cdc2904f65ce098bdf2de08c9037abb/PyInstaller/hooks/hook-PyQt5.QtWebEngineWidgets.py#L24\n-             bundle_identifier='org.qt-project.Qt.QtWebEngineCore')\n+             bundle_identifier='org.qutebrowser.qutebrowser')\ndiff --git a/misc/requirements/README.md b/misc/requirements/README.md\nindex 330233bca..d90a065e9 100644\n--- a/misc/requirements/README.md\n+++ b/misc/requirements/README.md\n@@ -19,4 +19,11 @@ Some examples:\n #@ filter: mypkg != 1.0.0\n #@ ignore: mypkg, otherpkg\n #@ replace: foo bar\n+\n+## Use the marker line to restrict the unpinned Flask requirement to python\n+## 3.7. For python 3.7 add a specific version into the output.\n+Flask\n+# Python 3.7\n+#@ markers: Flask python_version&gt;=\"3.7\"\n+#@ add: Flask==2.2.5 ; python_version==\"3.7.*\"\n ```\ndiff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt\nindex 1eee9b06c..29c5e8452 100644\n--- a/misc/requirements/requirements-check-manifest.txt\n+++ b/misc/requirements/requirements-check-manifest.txt\n@@ -1,8 +1,9 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-build==0.8.0\n-check-manifest==0.48\n-packaging==21.3\n-pep517==0.13.0\n-pyparsing==3.0.9\n+build==1.2.1\n+check-manifest==0.49\n+importlib_metadata==7.1.0\n+packaging==24.0\n+pyproject_hooks==1.1.0\n tomli==2.0.1\n+zipp==3.19.0\ndiff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt\nindex 777c78796..a85bc136f 100644\n--- a/misc/requirements/requirements-dev.txt\n+++ b/misc/requirements/requirements-dev.txt\n@@ -1,45 +1,48 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-bleach==5.0.1\n-build==0.8.0\n+backports.tarfile==1.1.1\n+build==1.2.1\n bump2version==1.0.1\n-certifi==2022.6.15\n-cffi==1.15.1\n-charset-normalizer==2.1.1\n-commonmark==0.9.1\n-cryptography==37.0.4\n-docutils==0.19\n-github3.py==3.2.0\n-hunter==3.4.3\n-idna==3.3\n-importlib-metadata==4.12.0\n+certifi==2024.2.2\n+cffi==1.16.0\n+charset-normalizer==3.3.2\n+cryptography==42.0.7\n+docutils==0.20.1\n+github3.py==4.0.1\n+hunter==3.7.0\n+idna==3.7\n+importlib_metadata==7.1.0\n+importlib_resources==6.4.0\n+jaraco.classes==3.4.0\n+jaraco.context==5.3.0\n+jaraco.functools==4.0.1\n jeepney==0.8.0\n-keyring==23.8.2\n+keyring==25.2.1\n manhole==1.8.0\n-packaging==21.3\n-pep517==0.13.0\n-pkginfo==1.8.3\n-ply==3.11\n-pycparser==2.21\n-Pygments==2.13.0\n-PyJWT==2.4.0\n+markdown-it-py==3.0.0\n+mdurl==0.1.2\n+more-itertools==10.2.0\n+nh3==0.2.17\n+packaging==24.0\n+pkginfo==1.10.0\n+pycparser==2.22\n+Pygments==2.18.0\n+PyJWT==2.8.0\n Pympler==1.0.1\n-pyparsing==3.0.9\n-PyQt-builder==1.13.0\n-python-dateutil==2.8.2\n-readme-renderer==37.0\n-requests==2.28.1\n-requests-toolbelt==0.9.1\n+pyproject_hooks==1.1.0\n+PyQt-builder==1.16.2\n+python-dateutil==2.9.0.post0\n+readme_renderer==43.0\n+requests==2.32.2\n+requests-toolbelt==1.0.0\n rfc3986==2.0.0\n-rich==12.5.1\n+rich==13.7.1\n SecretStorage==3.3.3\n-sip==6.6.2\n+sip==6.8.3\n six==1.16.0\n-toml==0.10.2\n tomli==2.0.1\n-twine==4.0.1\n-typing_extensions==4.3.0\n+twine==5.1.0\n+typing_extensions==4.12.0\n uritemplate==4.1.1\n-# urllib3==1.26.11\n-webencodings==0.5.1\n-zipp==3.8.1\n+# urllib3==2.2.1\n+zipp==3.19.0\ndiff --git a/misc/requirements/requirements-docs.txt b/misc/requirements/requirements-docs.txt\nnew file mode 100644\nindex 000000000..d2d35d758\n--- /dev/null\n+++ b/misc/requirements/requirements-docs.txt\n@@ -0,0 +1,3 @@\n+# This file is automatically generated by scripts/dev/recompile_requirements.py\n+\n+asciidoc==10.2.0\ndiff --git a/misc/requirements/requirements-docs.txt-raw b/misc/requirements/requirements-docs.txt-raw\nnew file mode 100644\nindex 000000000..1cd92d927\n--- /dev/null\n+++ b/misc/requirements/requirements-docs.txt-raw\n@@ -0,0 +1 @@\n+asciidoc\ndiff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt\nindex 78660a405..3044ce83b 100644\n--- a/misc/requirements/requirements-flake8.txt\n+++ b/misc/requirements/requirements-flake8.txt\n@@ -1,24 +1,23 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-attrs==22.1.0\n-flake8==5.0.4\n-flake8-bugbear==22.7.1\n-flake8-builtins==1.5.3\n-flake8-comprehensions==3.10.0\n-flake8-copyright==0.2.3\n+attrs==23.2.0\n+flake8==7.0.0\n+flake8-bugbear==24.4.26\n+flake8-builtins==2.5.0\n+flake8-comprehensions==3.14.0\n flake8-debugger==4.1.2\n-flake8-deprecated==1.3\n-flake8-docstrings==1.6.0\n+flake8-deprecated==2.2.1\n+flake8-docstrings==1.7.0\n flake8-future-import==0.4.7\n-flake8-plugin-utils==1.3.2\n-flake8-pytest-style==1.6.0\n+flake8-plugin-utils==1.3.3\n+flake8-pytest-style==2.0.0\n flake8-string-format==0.3.0\n-flake8-tidy-imports==4.8.0\n+flake8-tidy-imports==4.10.0\n flake8-tuple==0.4.1\n mccabe==0.7.0\n-pep8-naming==0.13.2\n-pycodestyle==2.9.1\n-pydocstyle==6.1.1\n-pyflakes==2.5.0\n+pep8-naming==0.14.1\n+pycodestyle==2.11.1\n+pydocstyle==6.3.0\n+pyflakes==3.2.0\n six==1.16.0\n snowballstemmer==2.2.0\ndiff --git a/misc/requirements/requirements-flake8.txt-raw b/misc/requirements/requirements-flake8.txt-raw\nindex de6bb733a..42bf76ee2 100644\n--- a/misc/requirements/requirements-flake8.txt-raw\n+++ b/misc/requirements/requirements-flake8.txt-raw\n@@ -3,9 +3,10 @@ flake8-bugbear\n flake8-builtins\n flake8-comprehensions\n flake8-debugger\n-flake8-deprecated\n+flake8-deprecated!=2.0.0\n flake8-docstrings\n-flake8-copyright\n+# https://github.com/savoirfairelinux/flake8-copyright/issues/19\n+# flake8-copyright\n flake8-future-import\n # https://github.com/aleGpereira/flake8-mock/issues/10\n # flake8-mock\ndiff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt\nindex d40954835..120a94932 100644\n--- a/misc/requirements/requirements-mypy.txt\n+++ b/misc/requirements/requirements-mypy.txt\n@@ -1,18 +1,21 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-chardet==5.0.0\n-diff-cover==6.5.1\n-importlib-metadata==4.12.0\n-importlib-resources==5.9.0\n-Jinja2==3.1.2\n-lxml==4.9.1\n-MarkupSafe==2.1.1\n-mypy==0.971\n-mypy-extensions==0.4.3\n-pluggy==1.0.0\n-Pygments==2.13.0\n+chardet==5.2.0\n+diff_cover==9.0.0\n+importlib_resources==6.4.0\n+Jinja2==3.1.4\n+lxml==5.2.2\n+MarkupSafe==2.1.5\n+mypy==1.10.0\n+mypy-extensions==1.0.0\n+pluggy==1.5.0\n+Pygments==2.18.0\n PyQt5-stubs==5.15.6.0\n tomli==2.0.1\n-types-PyYAML==6.0.11\n-typing_extensions==4.3.0\n-zipp==3.8.1\n+types-colorama==0.4.15.20240311\n+types-docutils==0.21.0.20240423\n+types-Pygments==2.18.0.20240506\n+types-PyYAML==6.0.12.20240311\n+types-setuptools==70.0.0.20240524\n+typing_extensions==4.12.0\n+zipp==3.19.0\ndiff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw\nindex dd00d3219..027f4fef6 100644\n--- a/misc/requirements/requirements-mypy.txt-raw\n+++ b/misc/requirements/requirements-mypy.txt-raw\n@@ -4,7 +4,8 @@ diff-cover\n \n PyQt5-stubs\n types-PyYAML\n+types-colorama\n+types-Pygments\n \n # So stubs are available even on newer Python versions\n importlib_resources\n-importlib_metadata\ndiff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt\nindex 3db372d82..b78b0c41c 100644\n--- a/misc/requirements/requirements-pyinstaller.txt\n+++ b/misc/requirements/requirements-pyinstaller.txt\n@@ -1,5 +1,8 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-altgraph==0.17.2\n-pyinstaller==5.3\n-pyinstaller-hooks-contrib==2022.8\n+altgraph==0.17.4\n+importlib_metadata==7.1.0\n+packaging==24.0\n+pyinstaller==6.7.0\n+pyinstaller-hooks-contrib==2024.6\n+zipp==3.19.0\ndiff --git a/misc/requirements/requirements-pyinstaller.txt-raw b/misc/requirements/requirements-pyinstaller.txt-raw\nindex c313980b0..ef376ca83 100644\n--- a/misc/requirements/requirements-pyinstaller.txt-raw\n+++ b/misc/requirements/requirements-pyinstaller.txt-raw\n@@ -1 +1 @@\n-PyInstaller\n+pyinstaller\ndiff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt\nindex acc335c55..10793cfb0 100644\n--- a/misc/requirements/requirements-pylint.txt\n+++ b/misc/requirements/requirements-pylint.txt\n@@ -1,30 +1,26 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-astroid==2.11.7\n-certifi==2022.6.15\n-cffi==1.15.1\n-charset-normalizer==2.1.1\n-cryptography==37.0.4\n-dill==0.3.5.1\n-future==0.18.2\n-github3.py==3.2.0\n-idna==3.3\n-isort==5.10.1\n-lazy-object-proxy==1.7.1\n+astroid==3.2.2\n+certifi==2024.2.2\n+cffi==1.16.0\n+charset-normalizer==3.3.2\n+cryptography==42.0.7\n+dill==0.3.8\n+github3.py==4.0.1\n+idna==3.7\n+isort==5.13.2\n mccabe==0.7.0\n-pefile==2022.5.30\n-platformdirs==2.5.2\n-pycparser==2.21\n-PyJWT==2.4.0\n-pylint==2.14.5\n-python-dateutil==2.8.2\n+pefile==2023.2.7\n+platformdirs==4.2.2\n+pycparser==2.22\n+PyJWT==2.8.0\n+pylint==3.2.2\n+python-dateutil==2.9.0.post0\n ./scripts/dev/pylint_checkers\n-requests==2.28.1\n+requests==2.32.2\n six==1.16.0\n tomli==2.0.1\n-tomlkit==0.11.4\n-typed-ast==1.5.4 ; python_version&lt;\"3.8\"\n-typing_extensions==4.3.0\n+tomlkit==0.12.5\n+typing_extensions==4.12.0\n uritemplate==4.1.1\n-# urllib3==1.26.11\n-wrapt==1.14.1\n+# urllib3==2.2.1\ndiff --git a/misc/requirements/requirements-pylint.txt-raw b/misc/requirements/requirements-pylint.txt-raw\nindex 54e12a02a..99a2cf02f 100644\n--- a/misc/requirements/requirements-pylint.txt-raw\n+++ b/misc/requirements/requirements-pylint.txt-raw\n@@ -1,11 +1,12 @@\n pylint\n+astroid\n ./scripts/dev/pylint_checkers\n requests\n github3.py\n pefile\n \n # fix qute-pylint location\n-#@ replace: qute-pylint.* ./scripts/dev/pylint_checkers\n+#@ replace: qute[_-]pylint.* ./scripts/dev/pylint_checkers\n #@ markers: typed-ast python_version&lt;\"3.8\"\n \n # Already included via test requirements\ndiff --git a/misc/requirements/requirements-pyqt-5.12.txt b/misc/requirements/requirements-pyqt-5.12.txt\ndeleted file mode 100644\nindex 0b1ea5ea3..000000000\n--- a/misc/requirements/requirements-pyqt-5.12.txt\n+++ /dev/null\n@@ -1,5 +0,0 @@\n-# This file is automatically generated by scripts/dev/recompile_requirements.py\n-\n-PyQt5==5.12.3  # rq.filter: &lt; 5.13\n-PyQt5-sip==12.11.0\n-PyQtWebEngine==5.12.1  # rq.filter: &lt; 5.13\ndiff --git a/misc/requirements/requirements-pyqt-5.12.txt-raw b/misc/requirements/requirements-pyqt-5.12.txt-raw\ndeleted file mode 100644\nindex f127ba42f..000000000\n--- a/misc/requirements/requirements-pyqt-5.12.txt-raw\n+++ /dev/null\n@@ -1,4 +0,0 @@\n-#@ filter: PyQt5 &lt; 5.13\n-#@ filter: PyQtWebEngine &lt; 5.13\n-PyQt5 &gt;= 5.12, &lt; 5.13\n-PyQtWebEngine &gt;= 5.12, &lt; 5.13\ndiff --git a/misc/requirements/requirements-pyqt-5.13.txt b/misc/requirements/requirements-pyqt-5.13.txt\ndeleted file mode 100644\nindex 4c693dc9a..000000000\n--- a/misc/requirements/requirements-pyqt-5.13.txt\n+++ /dev/null\n@@ -1,5 +0,0 @@\n-# This file is automatically generated by scripts/dev/recompile_requirements.py\n-\n-PyQt5==5.13.2  # rq.filter: &lt; 5.14\n-PyQt5-sip==12.11.0\n-PyQtWebEngine==5.13.2  # rq.filter: &lt; 5.14\ndiff --git a/misc/requirements/requirements-pyqt-5.13.txt-raw b/misc/requirements/requirements-pyqt-5.13.txt-raw\ndeleted file mode 100644\nindex e60db7edb..000000000\n--- a/misc/requirements/requirements-pyqt-5.13.txt-raw\n+++ /dev/null\n@@ -1,4 +0,0 @@\n-#@ filter: PyQt5 &lt; 5.14\n-#@ filter: PyQtWebEngine &lt; 5.14\n-PyQt5 &gt;= 5.13, &lt; 5.14\n-PyQtWebEngine &gt;= 5.13, &lt; 5.14\ndiff --git a/misc/requirements/requirements-pyqt-5.14.txt b/misc/requirements/requirements-pyqt-5.14.txt\ndeleted file mode 100644\nindex 1402afccd..000000000\n--- a/misc/requirements/requirements-pyqt-5.14.txt\n+++ /dev/null\n@@ -1,5 +0,0 @@\n-# This file is automatically generated by scripts/dev/recompile_requirements.py\n-\n-PyQt5==5.14.2  # rq.filter: &lt; 5.15\n-PyQt5-sip==12.11.0\n-PyQtWebEngine==5.14.0  # rq.filter: &lt; 5.15\ndiff --git a/misc/requirements/requirements-pyqt-5.14.txt-raw b/misc/requirements/requirements-pyqt-5.14.txt-raw\ndeleted file mode 100644\nindex 9dadfc846..000000000\n--- a/misc/requirements/requirements-pyqt-5.14.txt-raw\n+++ /dev/null\n@@ -1,4 +0,0 @@\n-#@ filter: PyQt5 &lt; 5.15\n-#@ filter: PyQtWebEngine &lt; 5.15\n-PyQt5 &gt;= 5.14, &lt; 5.15\n-PyQtWebEngine &gt;= 5.14, &lt; 5.15\ndiff --git a/misc/requirements/requirements-pyqt-5.15.0.txt b/misc/requirements/requirements-pyqt-5.15.0.txt\ndeleted file mode 100644\nindex 3f01584cc..000000000\n--- a/misc/requirements/requirements-pyqt-5.15.0.txt\n+++ /dev/null\n@@ -1,5 +0,0 @@\n-# This file is automatically generated by scripts/dev/recompile_requirements.py\n-\n-PyQt5==5.15.0  # rq.filter: == 5.15.0\n-PyQt5-sip==12.11.0\n-PyQtWebEngine==5.15.0  # rq.filter: == 5.15.0\ndiff --git a/misc/requirements/requirements-pyqt-5.15.0.txt-raw b/misc/requirements/requirements-pyqt-5.15.0.txt-raw\ndeleted file mode 100644\nindex 12d6adb7d..000000000\n--- a/misc/requirements/requirements-pyqt-5.15.0.txt-raw\n+++ /dev/null\n@@ -1,4 +0,0 @@\n-#@ filter: PyQt5 == 5.15.0\n-#@ filter: PyQtWebEngine == 5.15.0\n-PyQt5 == 5.15.0\n-PyQtWebEngine == 5.15.0\ndiff --git a/misc/requirements/requirements-pyqt-5.15.2.txt b/misc/requirements/requirements-pyqt-5.15.2.txt\nnew file mode 100644\nindex 000000000..41f75871e\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-5.15.2.txt\n@@ -0,0 +1,5 @@\n+# This file is automatically generated by scripts/dev/recompile_requirements.py\n+\n+PyQt5==5.15.2  # rq.filter: == 5.15.2\n+PyQt5-sip==12.13.0\n+PyQtWebEngine==5.15.2  # rq.filter: == 5.15.2\ndiff --git a/misc/requirements/requirements-pyqt-5.15.2.txt-raw b/misc/requirements/requirements-pyqt-5.15.2.txt-raw\nnew file mode 100644\nindex 000000000..f8475fd8a\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-5.15.2.txt-raw\n@@ -0,0 +1,4 @@\n+#@ filter: PyQt5 == 5.15.2\n+#@ filter: PyQtWebEngine == 5.15.2\n+PyQt5 == 5.15.2\n+PyQtWebEngine == 5.15.2\ndiff --git a/misc/requirements/requirements-pyqt-5.15.txt b/misc/requirements/requirements-pyqt-5.15.txt\nindex 29836f343..5f9e4828e 100644\n--- a/misc/requirements/requirements-pyqt-5.15.txt\n+++ b/misc/requirements/requirements-pyqt-5.15.txt\n@@ -1,7 +1,7 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-PyQt5==5.15.7  # rq.filter: &lt; 5.16\n+PyQt5==5.15.10  # rq.filter: &lt; 5.16\n PyQt5-Qt5==5.15.2\n-PyQt5-sip==12.11.0\n+PyQt5-sip==12.13.0\n PyQtWebEngine==5.15.6  # rq.filter: &lt; 5.16\n PyQtWebEngine-Qt5==5.15.2\ndiff --git a/misc/requirements/requirements-pyqt-5.txt b/misc/requirements/requirements-pyqt-5.txt\nnew file mode 100644\nindex 000000000..e8ee2b9c7\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-5.txt\n@@ -0,0 +1,7 @@\n+# This file is automatically generated by scripts/dev/recompile_requirements.py\n+\n+PyQt5==5.15.10\n+PyQt5-Qt5==5.15.2\n+PyQt5-sip==12.13.0\n+PyQtWebEngine==5.15.6\n+PyQtWebEngine-Qt5==5.15.2\ndiff --git a/misc/requirements/requirements-pyqt-5.txt-raw b/misc/requirements/requirements-pyqt-5.txt-raw\nnew file mode 100644\nindex 000000000..9c6afbf16\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-5.txt-raw\n@@ -0,0 +1,2 @@\n+PyQt5\n+PyQtWebEngine\ndiff --git a/misc/requirements/requirements-pyqt-6.2.txt b/misc/requirements/requirements-pyqt-6.2.txt\nnew file mode 100644\nindex 000000000..e90769ddd\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.2.txt\n@@ -0,0 +1,7 @@\n+# This file is automatically generated by scripts/dev/recompile_requirements.py\n+\n+PyQt6==6.2.3\n+PyQt6-Qt6==6.2.4\n+PyQt6-sip==13.6.0\n+PyQt6-WebEngine==6.2.1\n+PyQt6-WebEngine-Qt6==6.2.4\ndiff --git a/misc/requirements/requirements-pyqt-6.2.txt-raw b/misc/requirements/requirements-pyqt-6.2.txt-raw\nnew file mode 100644\nindex 000000000..ea182a474\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.2.txt-raw\n@@ -0,0 +1,4 @@\n+PyQt6 &gt;= 6.2, &lt; 6.3\n+PyQt6-Qt6 &gt;= 6.2, &lt; 6.3\n+PyQt6-WebEngine &gt;= 6.2, &lt; 6.3\n+PyQt6-WebEngine-Qt6 &gt;= 6.2, &lt; 6.3\ndiff --git a/misc/requirements/requirements-pyqt-6.3.txt b/misc/requirements/requirements-pyqt-6.3.txt\nnew file mode 100644\nindex 000000000..d82c623c3\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.3.txt\n@@ -0,0 +1,7 @@\n+# This file is automatically generated by scripts/dev/recompile_requirements.py\n+\n+PyQt6==6.3.1\n+PyQt6-Qt6==6.3.2\n+PyQt6-sip==13.6.0\n+PyQt6-WebEngine==6.3.1\n+PyQt6-WebEngine-Qt6==6.3.2\ndiff --git a/misc/requirements/requirements-pyqt-6.3.txt-raw b/misc/requirements/requirements-pyqt-6.3.txt-raw\nnew file mode 100644\nindex 000000000..b4fe4d66e\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.3.txt-raw\n@@ -0,0 +1,4 @@\n+PyQt6 &gt;= 6.3, &lt; 6.4\n+PyQt6-Qt6 &gt;= 6.3, &lt; 6.4\n+PyQt6-WebEngine &gt;= 6.3, &lt; 6.4\n+PyQt6-WebEngine-Qt6 &gt;= 6.3, &lt; 6.4\ndiff --git a/misc/requirements/requirements-pyqt-6.4.txt b/misc/requirements/requirements-pyqt-6.4.txt\nnew file mode 100644\nindex 000000000..b52e8a511\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.4.txt\n@@ -0,0 +1,7 @@\n+# This file is automatically generated by scripts/dev/recompile_requirements.py\n+\n+PyQt6==6.4.2\n+PyQt6-Qt6==6.4.3\n+PyQt6-sip==13.6.0\n+PyQt6-WebEngine==6.4.0\n+PyQt6-WebEngine-Qt6==6.4.3\ndiff --git a/misc/requirements/requirements-pyqt-6.4.txt-raw b/misc/requirements/requirements-pyqt-6.4.txt-raw\nnew file mode 100644\nindex 000000000..2de7ab852\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.4.txt-raw\n@@ -0,0 +1,4 @@\n+PyQt6 &gt;= 6.4, &lt; 6.5\n+PyQt6-Qt6 &gt;= 6.4, &lt; 6.5\n+PyQt6-WebEngine &gt;= 6.4, &lt; 6.5\n+PyQt6-WebEngine-Qt6 &gt;= 6.4, &lt; 6.5\ndiff --git a/misc/requirements/requirements-pyqt-6.5.txt b/misc/requirements/requirements-pyqt-6.5.txt\nnew file mode 100644\nindex 000000000..5dca9ab74\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.5.txt\n@@ -0,0 +1,7 @@\n+# This file is automatically generated by scripts/dev/recompile_requirements.py\n+\n+PyQt6==6.5.3\n+PyQt6-Qt6==6.5.3\n+PyQt6-sip==13.6.0\n+PyQt6-WebEngine==6.5.0\n+PyQt6-WebEngine-Qt6==6.5.3\ndiff --git a/misc/requirements/requirements-pyqt-6.5.txt-raw b/misc/requirements/requirements-pyqt-6.5.txt-raw\nnew file mode 100644\nindex 000000000..f2c9ea25a\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.5.txt-raw\n@@ -0,0 +1,4 @@\n+PyQt6 &gt;= 6.5, &lt; 6.6\n+PyQt6-Qt6 &gt;= 6.5, &lt; 6.6\n+PyQt6-WebEngine &gt;= 6.5, &lt; 6.6\n+PyQt6-WebEngine-Qt6 &gt;= 6.5, &lt; 6.6\ndiff --git a/misc/requirements/requirements-pyqt-6.6.txt b/misc/requirements/requirements-pyqt-6.6.txt\nnew file mode 100644\nindex 000000000..02f1a325f\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.6.txt\n@@ -0,0 +1,7 @@\n+# This file is automatically generated by scripts/dev/recompile_requirements.py\n+\n+PyQt6==6.6.1\n+PyQt6-Qt6==6.6.3\n+PyQt6-sip==13.6.0\n+PyQt6-WebEngine==6.6.0\n+PyQt6-WebEngine-Qt6==6.6.3\ndiff --git a/misc/requirements/requirements-pyqt-6.6.txt-raw b/misc/requirements/requirements-pyqt-6.6.txt-raw\nnew file mode 100644\nindex 000000000..7cfe6d34c\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.6.txt-raw\n@@ -0,0 +1,4 @@\n+PyQt6 &gt;= 6.6, &lt; 6.7\n+PyQt6-Qt6 &gt;= 6.6, &lt; 6.7\n+PyQt6-WebEngine &gt;= 6.6, &lt; 6.7\n+PyQt6-WebEngine-Qt6 &gt;= 6.6, &lt; 6.7\ndiff --git a/misc/requirements/requirements-pyqt-6.7.txt b/misc/requirements/requirements-pyqt-6.7.txt\nnew file mode 100644\nindex 000000000..cec31dba9\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.7.txt\n@@ -0,0 +1,8 @@\n+# This file is automatically generated by scripts/dev/recompile_requirements.py\n+\n+PyQt6==6.7.0\n+PyQt6-Qt6==6.7.1\n+PyQt6-sip==13.6.0\n+PyQt6-WebEngine==6.7.0\n+PyQt6-WebEngine-Qt6==6.7.1\n+--extra-index-url https://www.riverbankcomputing.com/pypi/simple/\ndiff --git a/misc/requirements/requirements-pyqt-6.7.txt-raw b/misc/requirements/requirements-pyqt-6.7.txt-raw\nnew file mode 100644\nindex 000000000..7df2d49c6\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.7.txt-raw\n@@ -0,0 +1,7 @@\n+PyQt6 &gt;= 6.7, &lt; 6.8\n+PyQt6-Qt6 &gt;= 6.7, &lt; 6.8\n+PyQt6-WebEngine &gt;= 6.7, &lt; 6.8\n+PyQt6-WebEngine-Qt6 &gt;= 6.7, &lt; 6.8\n+\n+# WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2024-April/045832.html\n+#@ add: --extra-index-url https://www.riverbankcomputing.com/pypi/simple/\ndiff --git a/misc/requirements/requirements-pyqt-6.txt b/misc/requirements/requirements-pyqt-6.txt\nnew file mode 100644\nindex 000000000..cec31dba9\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.txt\n@@ -0,0 +1,8 @@\n+# This file is automatically generated by scripts/dev/recompile_requirements.py\n+\n+PyQt6==6.7.0\n+PyQt6-Qt6==6.7.1\n+PyQt6-sip==13.6.0\n+PyQt6-WebEngine==6.7.0\n+PyQt6-WebEngine-Qt6==6.7.1\n+--extra-index-url https://www.riverbankcomputing.com/pypi/simple/\ndiff --git a/misc/requirements/requirements-pyqt-6.txt-raw b/misc/requirements/requirements-pyqt-6.txt-raw\nnew file mode 100644\nindex 000000000..16cc342cd\n--- /dev/null\n+++ b/misc/requirements/requirements-pyqt-6.txt-raw\n@@ -0,0 +1,7 @@\n+PyQt6\n+PyQt6-Qt6\n+PyQt6-WebEngine\n+PyQt6-WebEngine-Qt6\n+\n+# WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2024-April/045832.html\n+#@ add: --extra-index-url https://www.riverbankcomputing.com/pypi/simple/\ndiff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt\nindex f200c1737..cec31dba9 100644\n--- a/misc/requirements/requirements-pyqt.txt\n+++ b/misc/requirements/requirements-pyqt.txt\n@@ -1,7 +1,8 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-PyQt5==5.15.7\n-PyQt5-Qt5==5.15.2\n-PyQt5-sip==12.11.0\n-PyQtWebEngine==5.15.6\n-PyQtWebEngine-Qt5==5.15.2\n+PyQt6==6.7.0\n+PyQt6-Qt6==6.7.1\n+PyQt6-sip==13.6.0\n+PyQt6-WebEngine==6.7.0\n+PyQt6-WebEngine-Qt6==6.7.1\n+--extra-index-url https://www.riverbankcomputing.com/pypi/simple/\ndiff --git a/misc/requirements/requirements-pyqt.txt-raw b/misc/requirements/requirements-pyqt.txt-raw\nindex 9c6afbf16..16cc342cd 100644\n--- a/misc/requirements/requirements-pyqt.txt-raw\n+++ b/misc/requirements/requirements-pyqt.txt-raw\n@@ -1,2 +1,7 @@\n-PyQt5\n-PyQtWebEngine\n+PyQt6\n+PyQt6-Qt6\n+PyQt6-WebEngine\n+PyQt6-WebEngine-Qt6\n+\n+# WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2024-April/045832.html\n+#@ add: --extra-index-url https://www.riverbankcomputing.com/pypi/simple/\ndiff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt\nindex f7f97fba1..0a4e2fde7 100644\n--- a/misc/requirements/requirements-pyroma.txt\n+++ b/misc/requirements/requirements-pyroma.txt\n@@ -1,15 +1,17 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-build==0.8.0\n-certifi==2022.6.15\n-charset-normalizer==2.1.1\n-docutils==0.19\n-idna==3.3\n-packaging==21.3\n-pep517==0.13.0\n-Pygments==2.13.0\n-pyparsing==3.0.9\n-pyroma==4.0\n-requests==2.28.1\n+build==1.2.1\n+certifi==2024.2.2\n+charset-normalizer==3.3.2\n+docutils==0.20.1\n+idna==3.7\n+importlib_metadata==7.1.0\n+packaging==24.0\n+Pygments==2.18.0\n+pyproject_hooks==1.1.0\n+pyroma==4.2\n+requests==2.32.2\n tomli==2.0.1\n-urllib3==1.26.11\n+trove-classifiers==2024.5.22\n+urllib3==2.2.1\n+zipp==3.19.0\ndiff --git a/misc/requirements/requirements-qutebrowser.txt-raw b/misc/requirements/requirements-qutebrowser.txt-raw\nindex c628f528a..ca4081d1d 100644\n--- a/misc/requirements/requirements-qutebrowser.txt-raw\n+++ b/misc/requirements/requirements-qutebrowser.txt-raw\n@@ -2,23 +2,22 @@ Jinja2\n PyYAML\n \n ## Only used on macOS to make borderless windows resizable\n-pyobjc-core\n-pyobjc-framework-Cocoa\n+# Not needed anymore with Qt 6.3, but we can't express that\n+# here, and it won't hurt either.\n+## our recompile_requirements.py can't really deal with\n+## platform-specific dependencies unfortunately...\n+# pyobjc-core\n+# pyobjc-framework-Cocoa\n+#@ add: # Unpinned due to recompile_requirements.py limitations\n+#@ add: pyobjc-core ; sys_platform==\"darwin\"  \n+#@ add: pyobjc-framework-Cocoa ; sys_platform==\"darwin\"\n \n ## stdlib backports\n-importlib-resources\n+importlib_resources\n \n ## Optional dependencies\n Pygments  # For :view-source --pygments or on QtWebKit\n colorama  # Colored log output on Windows\n adblock  # Improved adblocking\n \n-# Optional, only relevant when installing PyQt5/PyQtWebEngine via pip.\n-importlib-metadata  # Determining PyQt version\n-typing_extensions  # from importlib-metadata\n-\n-#@ markers: importlib-resources python_version==\"3.7.*\" or python_version==\"3.8.*\"\n-#@ markers: importlib-metadata python_version==\"3.7.*\"\n-#@ markers: typing_extensions python_version&lt;\"3.8\"\n-#@ markers: pyobjc-core sys_platform==\"darwin\"\n-#@ markers: pyobjc-framework-Cocoa sys_platform==\"darwin\"\n+#@ markers: importlib_resources python_version==\"3.8.*\"\ndiff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt\nindex 50c892cf0..1a21cf2ab 100644\n--- a/misc/requirements/requirements-sphinx.txt\n+++ b/misc/requirements/requirements-sphinx.txt\n@@ -1,27 +1,26 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-alabaster==0.7.12\n-Babel==2.10.3\n-certifi==2022.6.15\n-charset-normalizer==2.1.1\n-docutils==0.19\n-idna==3.3\n+alabaster==0.7.13\n+Babel==2.15.0\n+certifi==2024.2.2\n+charset-normalizer==3.3.2\n+docutils==0.20.1\n+idna==3.7\n imagesize==1.4.1\n-importlib-metadata==4.12.0\n-Jinja2==3.1.2\n-MarkupSafe==2.1.1\n-packaging==21.3\n-Pygments==2.13.0\n-pyparsing==3.0.9\n-pytz==2022.2.1\n-requests==2.28.1\n+importlib_metadata==7.1.0\n+Jinja2==3.1.4\n+MarkupSafe==2.1.5\n+packaging==24.0\n+Pygments==2.18.0\n+pytz==2024.1\n+requests==2.32.2\n snowballstemmer==2.2.0\n-Sphinx==5.1.1\n-sphinxcontrib-applehelp==1.0.2\n+Sphinx==7.1.2\n+sphinxcontrib-applehelp==1.0.4\n sphinxcontrib-devhelp==1.0.2\n-sphinxcontrib-htmlhelp==2.0.0\n+sphinxcontrib-htmlhelp==2.0.1\n sphinxcontrib-jsmath==1.0.1\n sphinxcontrib-qthelp==1.0.3\n sphinxcontrib-serializinghtml==1.1.5\n-urllib3==1.26.11\n-zipp==3.8.1\n+urllib3==2.2.1\n+zipp==3.19.0\ndiff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt\nindex a7e55e32d..2baab1711 100644\n--- a/misc/requirements/requirements-tests.txt\n+++ b/misc/requirements/requirements-tests.txt\n@@ -1,59 +1,56 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-attrs==22.1.0\n-beautifulsoup4==4.11.1\n-certifi==2022.6.15\n-charset-normalizer==2.1.1\n-cheroot==8.6.0\n-click==8.1.3\n-coverage==6.4.4\n-exceptiongroup==1.0.0rc8\n-execnet==1.9.0\n-filelock==3.8.0\n-Flask==2.2.2\n-glob2==0.7\n-hunter==3.4.3\n-hypothesis==6.54.4\n-idna==3.3\n-importlib-metadata==4.12.0\n-iniconfig==1.1.1\n-itsdangerous==2.1.2\n-jaraco.functools==3.5.1\n-# Jinja2==3.1.2\n-Mako==1.2.1\n+attrs==23.2.0\n+beautifulsoup4==4.12.3\n+blinker==1.8.2\n+certifi==2024.2.2\n+charset-normalizer==3.3.2\n+cheroot==10.0.1\n+click==8.1.7\n+coverage==7.5.2\n+exceptiongroup==1.2.1\n+execnet==2.1.1\n+filelock==3.14.0\n+Flask==3.0.3\n+hunter==3.7.0\n+hypothesis==6.102.6\n+idna==3.7\n+importlib_metadata==7.1.0\n+iniconfig==2.0.0\n+itsdangerous==2.2.0\n+jaraco.functools==4.0.1\n+# Jinja2==3.1.4\n+Mako==1.3.5\n manhole==1.8.0\n-# MarkupSafe==2.1.1\n-more-itertools==8.14.0\n-packaging==21.3\n-parse==1.19.0\n-parse-type==0.6.0\n-pluggy==1.0.0\n-py==1.11.0\n-py-cpuinfo==8.0.0\n-Pygments==2.13.0\n-pyparsing==3.0.9\n-pytest==7.1.2\n-pytest-bdd==6.0.1\n-pytest-benchmark==3.4.1\n-pytest-cov==3.0.0\n-pytest-forked==1.4.0\n-pytest-instafail==0.4.2\n-pytest-mock==3.8.2\n-pytest-qt==4.1.0\n-pytest-repeat==0.9.1\n-pytest-rerunfailures==10.2\n-pytest-xdist==2.5.0\n-pytest-xvfb==2.0.0\n+# MarkupSafe==2.1.5\n+more-itertools==10.2.0\n+packaging==24.0\n+parse==1.20.1\n+parse-type==0.6.2\n+pluggy==1.5.0\n+py-cpuinfo==9.0.0\n+Pygments==2.18.0\n+pytest==8.2.1\n+pytest-bdd==7.1.2\n+pytest-benchmark==4.0.0\n+pytest-cov==5.0.0\n+pytest-instafail==0.5.0\n+pytest-mock==3.14.0\n+pytest-qt==4.4.0\n+pytest-repeat==0.9.3\n+pytest-rerunfailures==14.0\n+pytest-xdist==3.6.1\n+pytest-xvfb==3.0.0\n PyVirtualDisplay==3.0\n-requests==2.28.1\n-requests-file==1.5.1\n+requests==2.32.2\n+requests-file==2.1.0\n six==1.16.0\n sortedcontainers==2.4.0\n-soupsieve==2.3.2.post1\n-tldextract==3.3.1\n-toml==0.10.2\n+soupsieve==2.5\n+tldextract==5.1.2\n tomli==2.0.1\n-urllib3==1.26.11\n-vulture==2.5\n-Werkzeug==2.2.2\n-zipp==3.8.1\n+typing_extensions==4.12.0\n+urllib3==2.2.1\n+vulture==2.11\n+Werkzeug==3.0.3\n+zipp==3.19.0\ndiff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt\nindex e8975efda..edc096488 100644\n--- a/misc/requirements/requirements-tox.txt\n+++ b/misc/requirements/requirements-tox.txt\n@@ -1,16 +1,17 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-distlib==0.3.5\n-filelock==3.8.0\n-packaging==21.3\n-pip==22.2.2\n-platformdirs==2.5.2\n-pluggy==1.0.0\n-py==1.11.0\n-pyparsing==3.0.9\n-setuptools==65.2.0\n-six==1.16.0\n-toml==0.10.2\n-tox==3.25.1\n-virtualenv==20.16.3\n-wheel==0.37.1\n+cachetools==5.3.3\n+chardet==5.2.0\n+colorama==0.4.6\n+distlib==0.3.8\n+filelock==3.14.0\n+packaging==24.0\n+pip==24.0\n+platformdirs==4.2.2\n+pluggy==1.5.0\n+pyproject-api==1.6.1\n+setuptools==70.0.0\n+tomli==2.0.1\n+tox==4.15.0\n+virtualenv==20.26.2\n+wheel==0.43.0\ndiff --git a/misc/requirements/requirements-vulture.txt b/misc/requirements/requirements-vulture.txt\nindex 482a74195..a7d37e73a 100644\n--- a/misc/requirements/requirements-vulture.txt\n+++ b/misc/requirements/requirements-vulture.txt\n@@ -1,4 +1,4 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-toml==0.10.2\n-vulture==2.5\n+tomli==2.0.1\n+vulture==2.11\ndiff --git a/misc/requirements/requirements-yamllint.txt b/misc/requirements/requirements-yamllint.txt\nindex 78e80a261..4fb649ec4 100644\n--- a/misc/requirements/requirements-yamllint.txt\n+++ b/misc/requirements/requirements-yamllint.txt\n@@ -1,5 +1,5 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n-pathspec==0.9.0\n-PyYAML==6.0\n-yamllint==1.27.1\n+pathspec==0.12.1\n+PyYAML==6.0.1\n+yamllint==1.35.1\ndiff --git a/misc/userscripts/README.md b/misc/userscripts/README.md\nindex 7e247f4ba..6cc66dfb2 100644\n--- a/misc/userscripts/README.md\n+++ b/misc/userscripts/README.md\n@@ -60,8 +60,8 @@ The following userscripts can be found on their own repositories.\n   bookmark manager.\n - [qb-scripts](https://github.com/peterjschroeder/qb-scripts): a small pack of\n   userscripts.\n-- [instapaper.zsh](https://github.com/dmcgrady/instapaper.zsh): Add URL to\n-  your [Instapaper][] bookmark manager.\n+- [instapaper.zsh](https://github.com/vicentealencar/instapaper.zsh): Add URL to\n+  your [Instapaper][] bookmark manager (original repository by dmcgrady vanished).\n - [qtb.us](https://github.com/Chinggis6/qtb.us): small pack of userscripts.\n - [pinboard.zsh](https://github.com/dmix/pinboard.zsh): Add URL to your\n   [Pinboard][] bookmark manager.\n@@ -73,14 +73,14 @@ The following userscripts can be found on their own repositories.\n   selections via Google Translate.\n - [qute-snippets](https://github.com/Aledosim/qute-snippets): Bind text snippets to a keyword\n    and retrieve they when you want.\n-- [doi](https://github.com/cadadr/configuration/blob/master/qutebrowser/userscripts/doi):\n+- [doi](https://github.com/cadadr/configuration/blob/default/dotfiles/qutebrowser/userscripts/doi):\n   Opens DOIs on Sci-Hub.\n - [qute-1password](https://github.com/fmartingr/qute-1password):\n   Qutebrowser userscript to fill 1password credentials\n - [1password](https://github.com/tomoakley/dotfiles/blob/master/qutebrowser/userscripts/1password):\n   Integration with 1password on macOS.\n - [localhost](https://github.com/SidharthArya/.qutebrowser/blob/master/userscripts/localhost):\n-  Quickly navigate to localhost:port. For reference: [A quicker way to reach localhost with qutebrowser](https://sidhartharya.me/a-quicker-way-to-reach-localhost-with-qutebrowser/)\n+  Quickly navigate to localhost:port. For reference: [A quicker way to reach localhost with qutebrowser](https://blog.sidhartharya.com/a-quicker-way-to-reach-localhost-with-qutebrowser/)\n - [untrack-url](https://github.com/qutebrowser/qutebrowser/discussions/6555),\n   convert various URLs (YouTube/Reddit/Twitter/Instagram/Google Maps) to other\n   services (Invidious, Teddit, Nitter, Bibliogram, OpenStreetMap).\n@@ -100,6 +100,12 @@ The following userscripts can be found on their own repositories.\n - [qute-containers](https://github.com/s-praveen-kumar/qute-containers): \n   A simple interface to manage browser containers by manipulating the basedir\n   parameter.\n+- [qutebrowser-metascript](https://codeberg.org/mister_monster/qutebrowser-metascript):\n+  A user configurable arbitrary sequential command running userscript for qutebrowser\n+- [tab-manager](https://codeberg.org/mister_monster/tab-manager):\n+  More powerfully manage single window sessions\n+- [qutebrowser-url-mutator](https://codeberg.org/mister_monster/qutebrowser-url-mutator):\n+  automatically mutates input URLs based on configurable rules\n   \n [Zotero]: https://www.zotero.org/\n [Pocket]: https://getpocket.com/\ndiff --git a/misc/userscripts/add-nextcloud-bookmarks b/misc/userscripts/add-nextcloud-bookmarks\nindex 86f4f5bc7..2a480ccff 100755\n--- a/misc/userscripts/add-nextcloud-bookmarks\n+++ b/misc/userscripts/add-nextcloud-bookmarks\n@@ -41,7 +41,7 @@ from json import dumps\n from os import environ, path\n from sys import argv, exit\n \n-from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit\n+from PyQt6.QtWidgets import QApplication, QInputDialog, QLineEdit\n from requests import get, post\n from requests.auth import HTTPBasicAuth\n \n@@ -54,7 +54,7 @@ def get_text(name, info):\n             None,\n             \"add-nextcloud-bookmarks userscript\",\n             \"Please enter {}\".format(info),\n-            QLineEdit.Password,\n+            QLineEdit.EchoMode.Password,\n         )\n     else:\n         text, ok = QInputDialog.getText(\ndiff --git a/misc/userscripts/add-nextcloud-cookbook b/misc/userscripts/add-nextcloud-cookbook\nindex 3952bb16f..151090785 100755\n--- a/misc/userscripts/add-nextcloud-cookbook\n+++ b/misc/userscripts/add-nextcloud-cookbook\n@@ -37,7 +37,7 @@ import configparser\n from os import environ, path\n from sys import argv, exit\n \n-from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit\n+from PyQt6.QtWidgets import QApplication, QInputDialog, QLineEdit\n from requests import post\n from requests.auth import HTTPBasicAuth\n \n@@ -50,7 +50,7 @@ def get_text(name, info):\n             None,\n             \"add-nextcloud-cookbook userscript\",\n             \"Please enter {}\".format(info),\n-            QLineEdit.Password,\n+            QLineEdit.EchoMode.Password,\n         )\n     else:\n         text, ok = QInputDialog.getText(\ndiff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser\nindex 309736200..f481d00a5 100755\n--- a/misc/userscripts/dmenu_qutebrowser\n+++ b/misc/userscripts/dmenu_qutebrowser\n@@ -1,22 +1,9 @@\n #!/usr/bin/env bash\n \n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-# Copyright 2015 Zach-Button \n+# SPDX-FileCopyrightText: Zach-Button \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # Pipes history, quickmarks, and URL into dmenu.\n #\ndiff --git a/misc/userscripts/openfeeds b/misc/userscripts/openfeeds\nindex 7cd5b86a8..cfe765e42 100755\n--- a/misc/userscripts/openfeeds\n+++ b/misc/userscripts/openfeeds\n@@ -1,23 +1,10 @@\n #!/usr/bin/env python3\n # -*- coding: utf-8 -*-\n \n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-# Copyright 2015 jnphilipp \n+# SPDX-FileCopyrightText: jnphilipp \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # Opens all links to feeds defined in the head of a site\n #\ndiff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill\nindex 3ea8fd9f6..b63d36276 100755\n--- a/misc/userscripts/password_fill\n+++ b/misc/userscripts/password_fill\n@@ -143,6 +143,7 @@ no_entries_found() {\n #    expected to write the username of that entry to the $username variable and\n #    the corresponding password to $password\n \n+# shellcheck disable=SC2317\n reset_backend() {\n     init() { true ; }\n     query_entries() { true ; }\n@@ -198,7 +199,7 @@ choose_entry_zenity() {\n }\n \n choose_entry_zenity_radio() {\n-    zenity_helper() {\n+    zenity_helper() {  # shellcheck disable=SC2317\n         awk '{ print $0 ; print $0 }'                   \\\n         | zenity --list --radiolist                     \\\n                  --title \"qutebrowser password fill\"    \\\n@@ -278,6 +279,7 @@ pass_backend() {\n \n # =======================================================\n # backend: secret\n+# shellcheck disable=SC2317\n secret_backend() {\n     init() {\n         return\ndiff --git a/misc/userscripts/qute-1pass b/misc/userscripts/qute-1pass\nindex 091f841fc..19e5414a5 100755\n--- a/misc/userscripts/qute-1pass\n+++ b/misc/userscripts/qute-1pass\n@@ -2,7 +2,7 @@\n \n set +e\n \n-# JS field injection code from https://github.com/qutebrowser/qutebrowser/blob/master/misc/userscripts/password_fill\n+# JS field injection code from https://github.com/qutebrowser/qutebrowser/blob/main/misc/userscripts/password_fill\n javascript_escape() {\n     # print the first argument in an escaped way, such that it can safely\n     # be used within javascripts double quotes\ndiff --git a/misc/userscripts/qute-bitwarden b/misc/userscripts/qute-bitwarden\nindex a30f734f2..f9637bae9 100755\n--- a/misc/userscripts/qute-bitwarden\n+++ b/misc/userscripts/qute-bitwarden\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n \n-# Copyright 2017 Chris Braun (cryzed) \n-# Adapted for Bitwarden by Jonathan Haylett (JonnyHaystack) \n+# SPDX-FileCopyrightText: Chris Braun (cryzed) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"\n Insert login information using Bitwarden CLI and a dmenu-compatible application\ndiff --git a/misc/userscripts/qute-keepass b/misc/userscripts/qute-keepass\nindex 285377ffc..445d308d5 100755\n--- a/misc/userscripts/qute-keepass\n+++ b/misc/userscripts/qute-keepass\n@@ -1,21 +1,8 @@\n #!/usr/bin/env python3\n \n-# Copyright 2018-2021 Jay Kamat \n+# SPDX-FileCopyrightText: Jay Kamat \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"This userscript allows for insertion of usernames and passwords from keepass\n databases using pykeepass. Since it is a userscript, it must be run from\n@@ -42,7 +29,7 @@ you do not do this, you will get 'element not editable' errors.\n If keepass takes a while to open the DB, you might want to consider reducing\n the number of transform rounds in your database settings.\n \n-Dependencies: pykeepass (in python3), PyQt5. Without pykeepass, you will get an\n+Dependencies: pykeepass (in python3), PyQt6. Without pykeepass, you will get an\n exit code of 100.\n \n ********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!******************\n@@ -64,8 +51,8 @@ import shlex\n import subprocess\n import sys\n \n-from PyQt5.QtCore import QUrl\n-from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit\n+from PyQt6.QtCore import QUrl\n+from PyQt6.QtWidgets import QApplication, QInputDialog, QLineEdit\n \n try:\n     import pykeepass\n@@ -152,7 +139,7 @@ def get_password():\n     text, ok = QInputDialog.getText(\n         None, \"KeePass DB Password\",\n         \"Please enter your KeePass Master Password\",\n-        QLineEdit.Password)\n+        QLineEdit.EchoMode.Password)\n     if not ok:\n         stderr('Password Prompt Rejected.')\n         sys.exit(ExitCodes.USER_QUIT)\n@@ -247,10 +234,10 @@ def run(args):\n         # into insert-mode, so the form can be directly submitted by hitting\n         # enter afterwards. It doesn't matter when we go into insert mode, but\n         # the other commands need to be be executed sequentially, so we add\n-        # delays with later.\n+        # delays with cmd-later.\n         qute_command('insert-text {} ;;'\n-                     'later {} fake-key  ;;'\n-                     'later {} insert-text {}{}'\n+                     'cmd-later {} fake-key  ;;'\n+                     'cmd-later {} insert-text {}{}'\n                      .format(username, CMD_DELAY,\n                              CMD_DELAY * 2, password, insert_mode))\n \ndiff --git a/misc/userscripts/qute-keepassxc b/misc/userscripts/qute-keepassxc\nindex 61a6c7bce..d5970cfed 100755\n--- a/misc/userscripts/qute-keepassxc\n+++ b/misc/userscripts/qute-keepassxc\n@@ -2,20 +2,7 @@\n \n # Copyright (c) 2018-2021 Markus Bl\u00f6chl \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"\n # Introduction\n@@ -91,7 +78,7 @@ insert mode if you prefer that).\n [2]: https://qutebrowser.org/\n [3]: https://gnupg.org/\n [4]: https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-protocol.md\n-[5]: https://github.com/qutebrowser/qutebrowser/blob/master/doc/userscripts.asciidoc\n+[5]: https://github.com/qutebrowser/qutebrowser/blob/main/doc/userscripts.asciidoc\n [6]: https://keepassxc.org/docs/KeePassXC_GettingStarted.html#_setup_browser_integration\n \"\"\"\n \ndiff --git a/misc/userscripts/qute-lastpass b/misc/userscripts/qute-lastpass\nindex e99a51a0f..d79ef658a 100755\n--- a/misc/userscripts/qute-lastpass\n+++ b/misc/userscripts/qute-lastpass\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n \n-# Copyright 2017 Chris Braun (cryzed) \n-# Adapted for LastPass by Wayne Cheng (welps) \n+# SPDX-FileCopyrightText: Chris Braun (cryzed) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"\n Insert login information using lastpass CLI and a dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...).\ndiff --git a/misc/userscripts/qute-pass b/misc/userscripts/qute-pass\nindex bdee94bb5..70a497b63 100755\n--- a/misc/userscripts/qute-pass\n+++ b/misc/userscripts/qute-pass\n@@ -1,21 +1,8 @@\n #!/usr/bin/env python3\n \n-# Copyright 2017 Chris Braun (cryzed) \n+# SPDX-FileCopyrightText: Chris Braun (cryzed) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"\n Insert login information using pass and a dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...). A short\n@@ -58,6 +45,7 @@ import re\n import shlex\n import subprocess\n import sys\n+from urllib.parse import urlparse\n \n import tldextract\n \n@@ -221,7 +209,7 @@ def main(arguments):\n \n     # Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains),\n     # the registered domain name, the IPv4 address if that's what the URL represents and finally the private domain\n-    # (if a non-public suffix was used).\n+    # (if a non-public suffix was used), and the URL netloc.\n     candidates = set()\n     attempted_targets = []\n \n@@ -230,7 +218,9 @@ def main(arguments):\n         private_domain = ('.'.join((extract_result.subdomain, extract_result.domain))\n                           if extract_result.subdomain else extract_result.domain)\n \n-    for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.ipv4, private_domain]):\n+    netloc = urlparse(arguments.url).netloc\n+\n+    for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.ipv4, private_domain, netloc]):\n         attempted_targets.append(target)\n         target_candidates = find_pass_candidates(target, unfiltered=arguments.unfiltered)\n         if not target_candidates:\ndiff --git a/misc/userscripts/readability b/misc/userscripts/readability\nindex 19b687939..07095a5b8 100755\n--- a/misc/userscripts/readability\n+++ b/misc/userscripts/readability\n@@ -47,7 +47,7 @@ with codecs.open(os.environ['QUTE_HTML'], 'r', 'utf-8') as source:\n \n     try:\n         from breadability.readable import Article as reader\n-        doc = reader(data)\n+        doc = reader(data, os.environ['QUTE_URL'])\n         title = doc._original_document.title\n         content = HEADER % title + doc.readable + \"\"\n     except ImportError:\ndiff --git a/misc/userscripts/rss b/misc/userscripts/rss\nindex 3c52d1f27..fab57fef6 100755\n--- a/misc/userscripts/rss\n+++ b/misc/userscripts/rss\n@@ -1,21 +1,8 @@\n #!/bin/sh\n \n-# Copyright 2016 Jan Verbeek (blyxxyz) \n+# SPDX-FileCopyrightText: Jan Verbeek (blyxxyz) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # This script keeps track of URLs in RSS feeds and opens new ones.\n # New feeds can be added with ':spawn -u /path/to/userscripts/rss add' or\ndiff --git a/misc/userscripts/taskadd b/misc/userscripts/taskadd\nindex 3f5368b92..222592f79 100755\n--- a/misc/userscripts/taskadd\n+++ b/misc/userscripts/taskadd\n@@ -11,7 +11,7 @@\n #       :spawn --userscript taskadd due:eod pri:H\n #\n #   To enable passing along extra args, I suggest using a mapping like:\n-#       :bind  set-cmd-text -s :spawn --userscript taskadd\n+#       :bind  cmd-set-text -s :spawn --userscript taskadd\n #\n #   If run from hint mode, it uses the selected hint text as the description\n #   and the selected hint url as the annotation.\ndiff --git a/misc/userscripts/tor_identity b/misc/userscripts/tor_identity\nindex a08f4f006..a6d7c9250 100755\n--- a/misc/userscripts/tor_identity\n+++ b/misc/userscripts/tor_identity\n@@ -1,27 +1,14 @@\n #!/usr/bin/env python3\n # -*- coding: utf-8 -*-\n \n-# Copyright 2018-2021 J. Nathanael Philipp (jnphilipp) \n+# SPDX-FileCopyrightText: J. Nathanael Philipp (jnphilipp) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # Change your tor identity.\n #\n # Set a hotkey to launch this script, then:\n-#   :bind ti spawn --userscript tor_identity PASSWORD\n+#   :bind ti spawn --userscript tor_identity -p PASSWORD\n #\n # Use the hotkey to change your tor identity, press 'ti' to change it.\n # https://stem.torproject.org/faq.html#how-do-i-request-a-new-identity-from-tor\n@@ -45,7 +32,7 @@ except ImportError:\n \n if __name__ == '__main__':\n     parser = ArgumentParser(prog='tor_identity')\n-    parser.add_argument('-c', '--control-port', default=9051,\n+    parser.add_argument('-c', '--control-port', type=int, default=9051,\n                         help='Tor control port (default 9051).')\n     parser.add_argument('-p', '--password', type=str, default=None,\n                         help='Tor control port password.')\ndiff --git a/misc/userscripts/view_in_mpv b/misc/userscripts/view_in_mpv\nindex 472920433..4f371c6b5 100755\n--- a/misc/userscripts/view_in_mpv\n+++ b/misc/userscripts/view_in_mpv\n@@ -49,7 +49,7 @@ msg() {\n \n MPV_COMMAND=${MPV_COMMAND:-mpv}\n # Warning: spaces in single flags are not supported\n-MPV_FLAGS=${MPV_FLAGS:- --force-window --no-terminal --keep-open=yes --ytdl}\n+MPV_FLAGS=${MPV_FLAGS:- --force-window --quiet --keep-open=yes --ytdl}\n IFS=\" \" read -r -a video_command &lt;&lt;&lt; \"$MPV_COMMAND $MPV_FLAGS\"\n \n js() {\n@@ -94,9 +94,9 @@ cat &lt;click here.\n             \n         \";\n@@ -119,7 +119,6 @@ cat &lt;&gt; \"$QUTE_FIFO\"\n+echo \"jseval -q -w main $(printjs)\" &gt;&gt; \"$QUTE_FIFO\"\n \n msg info \"Opening $QUTE_URL with mpv\"\n \"${video_command[@]}\" \"$@\" \"$QUTE_URL\"\ndiff --git a/pyrightconfig.json b/pyrightconfig.json\nnew file mode 100644\nindex 000000000..6371938c8\n--- /dev/null\n+++ b/pyrightconfig.json\n@@ -0,0 +1,11 @@\n+{\n+  \"defineConstant\": {\n+    \"USE_PYQT6\": true,\n+    \"USE_PYQT5\": false,\n+    \"USE_PYSIDE6\": false,\n+    \"IS_QT5\": false,\n+    \"IS_QT6\": true,\n+    \"IS_PYQT\": true,\n+    \"IS_PYSIDE\": false\n+  }\n+}\ndiff --git a/pytest.ini b/pytest.ini\nindex d32746281..71a6f606d 100644\n--- a/pytest.ini\n+++ b/pytest.ini\n@@ -27,7 +27,6 @@ markers =\n     no_ci: Tests which should not run on CI.\n     qtwebengine_todo: Features still missing with QtWebEngine\n     qtwebengine_skip: Tests not applicable with QtWebEngine\n-    qtwebengine_notifications: Tests which need QtWebEngine notification support\n     qtwebkit_skip: Tests not applicable with QtWebKit\n     qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine\n     qtwebengine_mac_xfail: Tests which fail on macOS with QtWebEngine\n@@ -35,60 +34,48 @@ markers =\n     no_invalid_lines: Don't fail on unparsable lines in end2end tests\n     fake_os: Fake utils.is_* to a fake operating system\n     unicode_locale: Tests which need a unicode locale to work\n-    js_headers: Sets JS headers dynamically on QtWebEngine (unsupported on some versions)\n     qtwebkit_pdf_imageformat_skip: Broken on QtWebKit with PDF image format plugin installed\n+    qtwebkit_openssl3_skip: Broken due to cheroot bug with OpenSSL 3 on QtWebKit\n     windows_skip: Tests which should be skipped on Windows\n+    qt5_only: Tests which should only run with Qt 5\n+    qt6_only: Tests which should only run with Qt 6\n+    qt5_xfail: Tests which fail with Qt 5\n+    qt6_xfail: Tests which fail with Qt 6\n qt_log_level_fail = WARNING\n qt_log_ignore =\n-    ^SpellCheck: .*\n-    ^SetProcessDpiAwareness failed: .*\n+    # GitHub Actions\n+    ^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .*\n+    # test_on_focus_changed_issue1484 on macOS\n+    ^The available OpenGL surface format was either not version 3\\.2 or higher or not a Core Profile.*\n+    # tests/unit/mainwindow/test_messageview.py and\n+    # tests/unit/mainwindow/statusbar/test_textbase.py::test_resize\n+    # on Windows\n     ^QWindowsWindow::setGeometry(Dp)?: Unable to set geometry .*\n+    # tests/unit/commands/test_userscripts.py::test_killed_command\n+    # on Windows\n     ^QProcess: Destroyed while process .* is still running\\.\n-    ^\"Method \"GetAll\" with signature \"s\" on interface \"org\\.freedesktop\\.DBus\\.Properties\" doesn't exist\n-    ^\"Method \\\\\"GetAll\\\\\" with signature \\\\\"s\\\\\" on interface \\\\\"org\\.freedesktop\\.DBus\\.Properties\\\\\" doesn't exist\\\\n\"\n-    ^propsReply \"Method \\\\\"GetAll\\\\\" with signature \\\\\"s\\\\\" on interface \\\\\"org\\.freedesktop\\.DBus\\.Properties\\\\\" doesn't exist\\\\n\"\n-    ^nmReply \"Method \\\\\"GetDevices\\\\\" with signature \\\\\"\\\\\" on interface \\\\\"org\\.freedesktop\\.NetworkManager\\\\\" doesn't exist\\\\n\"\n-    ^\"Object path cannot be empty\"\n-    ^virtual void QSslSocketBackendPrivate::transmit\\(\\) SSL write failed with error: -9805\n-    ^virtual void QSslSocketBackendPrivate::transmit\\(\\) SSLRead failed with: -9805\n-    ^Type conversion already registered from type .*\n-    ^QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once\\.\n-    ^QWaitCondition: Destroyed while threads are still waiting\n-    ^QXcbXSettings::QXcbXSettings\\(QXcbScreen\\*\\) Failed to get selection owner for XSETTINGS_S atom\n-    ^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .*\n-    ^QObject::connect: Cannot connect \\(null\\)::stateChanged\\(QNetworkSession::State\\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\\(QNetworkSession::State\\)\n-    ^QXcbClipboard: Cannot transfer data, no data available\n-    ^load glyph failed\n-    ^Error when parsing the netrc file\n-    ^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=\n-    ^QPainter::end: Painter ended with \\d+ saved states\n+    # Qt 6.5 debug build\n+    # WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2023-March/045215.html\n+    ^QObject::connect: Connecting from COMPAT signal \\(QSocketNotifier::activated\\(int\\)\\)$\n+    # Randomly started showing up on Qt 5.15.2\n+    ^QBackingStore::endPaint\\(\\) called with active painter; did you forget to destroy it or call QPainter::end\\(\\) on it\\?$\n+    # Qt 6.5 after system update, from qt-qt.accessibility.atspi\n+    Error in contacting registry: \"org\\.freedesktop\\.DBus\\.Error\\.Disconnected\" \"Not connected to D-Bus server\"\n+    # Seen in Qt 6.6.2 on CI, https://github.com/qutebrowser/qutebrowser/pull/8106#issuecomment-1952320663\n+    ^QDBusConnection: couldn't handle call to Notify, no slot matched\n+    ^QDBusConnection: couldn't handle call to CloseNotification, no slot matched\n+    # Qt 6.7\n+    ^Path override failed for key base::DIR_APP_DICTIONARIES and path '.*/qtwebengine_dictionaries'\n+    # Sometime the above message gets printed twice at the same time and the messages get interleaved.\n+    # The last part of the outer message gets bumped down to a line on its own, so hopefully this\n+    # catches that. And we don't see any other weird permutations of this.\n+    ^[^ ]*qtwebengine_dictionaries'$\n+    # Qt 5 on Archlinux\n     ^QSslSocket: cannot resolve .*\n-    ^QSslSocket: cannot call unresolved function .*\n-    ^Incompatible version of OpenSSL\n-    ^QQuickWidget::invalidateRenderControl could not make context current\n-    ^libpng warning: iCCP: known incorrect sRGB profile\n-    ^inotify_add_watch\\(\".*\"\\) failed: \"No space left on device\"\n-    ^QSettings::value: Empty key passed\n-    ^Icon theme \".*\" not found\n-    ^Error receiving trust for a CA certificate\n-    ^QBackingStore::endPaint\\(\\) called with active painter.*\n-    ^QPaintDevice: Cannot destroy paint device that is being painted\n-    ^DirectWrite: CreateFontFaceFromHDC\\(\\) failed .*\n-    ^Attribute Qt::AA_ShareOpenGLContexts must be set before QCoreApplication is created\\.\n-    ^QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not de-queue request, failed to report HostNotFoundError\n-    ^The available OpenGL surface format was either not version 3\\.2 or higher or not a Core Profile.*\n xfail_strict = true\n filterwarnings =\n     error\n     default:Test process .* failed to terminate!:UserWarning\n-    ignore:_SixMetaPathImporter\\.exec_module\\(\\) not found; falling back to load_module\\(\\):ImportWarning\n-    ignore:VendorImporter\\.find_spec\\(\\) not found; falling back to find_module\\(\\):ImportWarning\n-    ignore:_SixMetaPathImporter\\.find_spec\\(\\) not found; falling back to find_module\\(\\):ImportWarning\n-    # https://github.com/ionelmc/python-hunter/issues/97\n-    ignore:The distutils\\.sysconfig module is deprecated, use sysconfig instead:DeprecationWarning\n-    # https://github.com/certifi/python-certifi/issues/170\n-    ignore:path is deprecated\\. Use files\\(\\) instead\\. Refer to https.//importlib-resources\\.readthedocs\\.io/en/latest/using\\.html#migrating-from-legacy for migration advice\\.:DeprecationWarning:certifi.core\n-    # https://github.com/HypothesisWorks/hypothesis/issues/3309\n-    ignore:module 'sre_constants' is deprecated:DeprecationWarning\n-    ignore:module 'sre_parse' is deprecated:DeprecationWarning\n+    # Python 3.12: https://github.com/ionelmc/pytest-benchmark/issues/240 (fixed but not released)\n+    ignore:(datetime\\.)?datetime\\.utcnow\\(\\) is deprecated and scheduled for removal in a future version\\. Use timezone-aware objects to represent datetimes in UTC. (datetime\\.)?datetime\\.now\\(datetime\\.UTC\\)\\.:DeprecationWarning:pytest_benchmark\\.utils\n faulthandler_timeout = 90\ndiff --git a/qutebrowser.py b/qutebrowser.py\nindex 89ea27dcb..bdd5e574f 100755\n--- a/qutebrowser.py\n+++ b/qutebrowser.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Simple launcher for qutebrowser.\"\"\"\n \ndiff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py\nindex df320f996..c22b0b541 100644\n--- a/qutebrowser/__init__.py\n+++ b/qutebrowser/__init__.py\n@@ -1,32 +1,20 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"A keyboard-driven, vim-like browser based on Python and Qt.\"\"\"\n \n import os.path\n+import datetime\n+\n+_year = datetime.date.today().year\n \n __author__ = \"Florian Bruhin\"\n-__copyright__ = \"Copyright 2014-2021 Florian Bruhin (The Compiler)\"\n+__copyright__ = f\"Copyright 2013-{_year} Florian Bruhin (The Compiler)\"\n __license__ = \"GPL\"\n __maintainer__ = __author__\n __email__ = \"mail@qutebrowser.org\"\n-__version__ = \"2.5.2\"\n+__version__ = \"3.1.0\"\n __version_info__ = tuple(int(part) for part in __version__.split('.'))\n __description__ = \"A keyboard-driven, vim-like browser based on Python and Qt.\"\n \ndiff --git a/qutebrowser/__main__.py b/qutebrowser/__main__.py\nindex 89ea27dcb..bdd5e574f 100644\n--- a/qutebrowser/__main__.py\n+++ b/qutebrowser/__main__.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Simple launcher for qutebrowser.\"\"\"\n \ndiff --git a/qutebrowser/api/__init__.py b/qutebrowser/api/__init__.py\nindex 15f4f8f0f..cd511f2fc 100644\n--- a/qutebrowser/api/__init__.py\n+++ b/qutebrowser/api/__init__.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"API for extensions.\n \ndiff --git a/qutebrowser/api/apitypes.py b/qutebrowser/api/apitypes.py\nindex cbbd4467c..5aacf80b4 100644\n--- a/qutebrowser/api/apitypes.py\n+++ b/qutebrowser/api/apitypes.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"A single tab.\"\"\"\n \ndiff --git a/qutebrowser/api/cmdutils.py b/qutebrowser/api/cmdutils.py\nindex 73c6a1bc5..e5466f072 100644\n--- a/qutebrowser/api/cmdutils.py\n+++ b/qutebrowser/api/cmdutils.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"qutebrowser has the concept of functions, exposed to the user as commands.\n \n@@ -50,7 +35,7 @@ Possible values:\n \n \n import inspect\n-from typing import Any, Callable, Iterable\n+from typing import Any, Callable, Iterable, Protocol, Optional, Dict, cast\n \n from qutebrowser.utils import qtutils\n from qutebrowser.commands import command, cmdexc\n@@ -105,7 +90,21 @@ def check_exclusive(flags: Iterable[bool], names: Iterable[str]) -&gt; None:\n         raise CommandError(\"Only one of {} can be given!\".format(argstr))\n \n \n-_CmdHandlerType = Callable[..., Any]\n+_CmdHandlerFunc = Callable[..., Any]\n+\n+\n+class _CmdHandlerType(Protocol):\n+\n+    \"\"\"A qutebrowser command function, which had qute_args patched on it.\n+\n+    Applying @cmdutils.argument to a function will patch it with a qute_args attribute.\n+    Below, we cast the decorated function to _CmdHandlerType to make mypy aware of this.\n+    \"\"\"\n+\n+    qute_args: Optional[Dict[str, 'command.ArgInfo']]\n+\n+    def __call__(self, *args: Any, **kwargs: Any) -&gt; Any:\n+        ...\n \n \n class register:  # noqa: N801,N806 pylint: disable=invalid-name\n@@ -133,7 +132,7 @@ class register:  # noqa: N801,N806 pylint: disable=invalid-name\n         # The arguments to pass to Command.\n         self._kwargs = kwargs\n \n-    def __call__(self, func: _CmdHandlerType) -&gt; _CmdHandlerType:\n+    def __call__(self, func: _CmdHandlerFunc) -&gt; _CmdHandlerType:\n         \"\"\"Register the command before running the function.\n \n         Gets called when a function should be decorated.\n@@ -173,7 +172,8 @@ class register:  # noqa: N801,N806 pylint: disable=invalid-name\n \n         # This is checked by future @cmdutils.argument calls so they fail\n         # (as they'd be silently ignored otherwise)\n-        func.qute_args = None  # type: ignore[attr-defined]\n+        func = cast(_CmdHandlerType, func)\n+        func.qute_args = None\n \n         return func\n \n@@ -225,19 +225,21 @@ class argument:  # noqa: N801,N806 pylint: disable=invalid-name\n         self._argname = argname   # The name of the argument to handle.\n         self._kwargs = kwargs  # Valid ArgInfo members.\n \n-    def __call__(self, func: _CmdHandlerType) -&gt; _CmdHandlerType:\n+    def __call__(self, func: _CmdHandlerFunc) -&gt; _CmdHandlerType:\n         funcname = func.__name__\n \n         if self._argname not in inspect.signature(func).parameters:\n             raise ValueError(\"{} has no argument {}!\".format(funcname,\n                                                              self._argname))\n+\n+        func = cast(_CmdHandlerType, func)\n         if not hasattr(func, 'qute_args'):\n-            func.qute_args = {}  # type: ignore[attr-defined]\n-        elif func.qute_args is None:  # type: ignore[attr-defined]\n+            func.qute_args = {}\n+        elif func.qute_args is None:\n             raise ValueError(\"@cmdutils.argument got called above (after) \"\n                              \"@cmdutils.register for {}!\".format(funcname))\n \n         arginfo = command.ArgInfo(**self._kwargs)\n-        func.qute_args[self._argname] = arginfo  # type: ignore[attr-defined]\n+        func.qute_args[self._argname] = arginfo\n \n         return func\ndiff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py\nindex fd0890602..201f95c14 100644\n--- a/qutebrowser/api/config.py\n+++ b/qutebrowser/api/config.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Access to the qutebrowser configuration.\"\"\"\n \ndiff --git a/qutebrowser/api/downloads.py b/qutebrowser/api/downloads.py\nindex 85f5b8b91..391a4c13e 100644\n--- a/qutebrowser/api/downloads.py\n+++ b/qutebrowser/api/downloads.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"APIs related to downloading files.\"\"\"\n \ndiff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py\nindex 884d6c67f..9a1a7bc9c 100644\n--- a/qutebrowser/api/hook.py\n+++ b/qutebrowser/api/hook.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # pylint: disable=invalid-name\n \ndiff --git a/qutebrowser/api/interceptor.py b/qutebrowser/api/interceptor.py\nindex edbfe36a7..e0dbf66ec 100644\n--- a/qutebrowser/api/interceptor.py\n+++ b/qutebrowser/api/interceptor.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"APIs related to intercepting/blocking requests.\"\"\"\n \ndiff --git a/qutebrowser/api/message.py b/qutebrowser/api/message.py\nindex 068d06a38..3e1dd6469 100644\n--- a/qutebrowser/api/message.py\n+++ b/qutebrowser/api/message.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utilities to display messages above the status bar.\"\"\"\n \ndiff --git a/qutebrowser/api/qtutils.py b/qutebrowser/api/qtutils.py\nindex c404530ba..ee1dc2f36 100644\n--- a/qutebrowser/api/qtutils.py\n+++ b/qutebrowser/api/qtutils.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2019-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utilities related to Qt classes.\"\"\"\n \ndiff --git a/qutebrowser/app.py b/qutebrowser/app.py\nindex 3bebdf855..51603a2b9 100644\n--- a/qutebrowser/app.py\n+++ b/qutebrowser/app.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Initialization of qutebrowser and application-wide things.\n \n@@ -44,8 +29,9 @@ import tempfile\n import pathlib\n import datetime\n import argparse\n-from typing import Iterable, Optional\n+from typing import Iterable, Optional, List, Tuple\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.widgets import QApplication, QWidget\n from qutebrowser.qt.gui import QDesktopServices, QPixmap, QIcon\n from qutebrowser.qt.core import pyqtSlot, QUrl, QObject, QEvent, pyqtSignal, Qt\n@@ -64,7 +50,7 @@ from qutebrowser.keyinput import macros, eventfilter\n from qutebrowser.mainwindow import mainwindow, prompt, windowundo\n from qutebrowser.misc import (ipc, savemanager, sessions, crashsignal,\n                               earlyinit, sql, cmdhistory, backendproblem,\n-                              objects, quitter)\n+                              objects, quitter, nativeeventfilter)\n from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,\n                                resources, usertypes, standarddir,\n                                error, qtutils, debug)\n@@ -146,10 +132,12 @@ def init(*, args: argparse.Namespace) -&gt; None:\n     crashsignal.crash_handler.init_faulthandler()\n \n     objects.qapp.setQuitOnLastWindowClosed(False)\n+    # WORKAROUND for KDE file dialogs / QEventLoopLocker quitting:\n+    # https://bugreports.qt.io/browse/QTBUG-124386\n+    objects.qapp.setQuitLockEnabled(False)\n     quitter.instance.shutting_down.connect(QApplication.closeAllWindows)\n \n     _init_icon()\n-    _init_pulseaudio()\n \n     loader.init()\n     loader.load_components()\n@@ -195,27 +183,12 @@ def _init_icon():\n         objects.qapp.setWindowIcon(icon)\n \n \n-def _init_pulseaudio():\n-    \"\"\"Set properties for PulseAudio.\n-\n-    WORKAROUND for https://bugreports.qt.io/browse/QTBUG-85363\n-\n-    Affected Qt versions:\n-    - Older than 5.11 (which is unsupported)\n-    - 5.14.0 to 5.15.0 (inclusive)\n-\n-    However, we set this on all versions so that qutebrowser's icon gets picked\n-    up as well.\n-    \"\"\"\n-    for prop in ['application.name', 'application.icon_name']:\n-        os.environ['PULSE_PROP_OVERRIDE_' + prop] = 'qutebrowser'\n-\n-\n def _process_args(args):\n     \"\"\"Open startpage etc. and process commandline args.\"\"\"\n     if not args.override_restore:\n         sessions.load_default(args.session)\n \n+    new_window = None\n     if not sessions.session_manager.did_load:\n         log.init.debug(\"Initializing main window...\")\n         private = args.target == 'private-window'\n@@ -226,15 +199,17 @@ def _process_args(args):\n             error.handle_fatal_exc(err, 'Cannot start in private mode',\n                                    no_err_windows=args.no_err_windows)\n             sys.exit(usertypes.Exit.err_init)\n-        window = mainwindow.MainWindow(private=private)\n-        if not args.nowindow:\n-            window.show()\n-        objects.qapp.setActiveWindow(window)\n+\n+        new_window = mainwindow.MainWindow(private=private)\n \n     process_pos_args(args.command)\n     _open_startpage()\n     _open_special_pages(args)\n \n+    if new_window is not None and not args.nowindow:\n+        new_window.show()\n+        objects.qapp.setActiveWindow(new_window)\n+\n     delta = datetime.datetime.now() - earlyinit.START_TIME\n     log.init.debug(\"Init finished after {}s\".format(delta.total_seconds()))\n \n@@ -258,26 +233,30 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):\n     if command_target in {'window', 'private-window'}:\n         command_target = 'tab-silent'\n \n-    win_id: Optional[int] = None\n+    window: Optional[mainwindow.MainWindow] = None\n \n     if via_ipc and (not args or args == ['']):\n-        win_id = mainwindow.get_window(via_ipc=via_ipc,\n-                                       target=new_window_target)\n-        _open_startpage(win_id)\n+        window = mainwindow.get_window(via_ipc=via_ipc, target=new_window_target)\n+        _open_startpage(window)\n+        window.show()\n+        window.maybe_raise()\n         return\n \n     for cmd in args:\n         if cmd.startswith(':'):\n-            if win_id is None:\n-                win_id = mainwindow.get_window(via_ipc=via_ipc,\n-                                               target=command_target)\n+            if window is None:\n+                window = mainwindow.get_window(via_ipc=via_ipc, target=command_target)\n+                # FIXME preserving old behavior, but we probably shouldn't be\n+                # doing this...\n+                # See https://github.com/qutebrowser/qutebrowser/issues/5094\n+                window.maybe_raise()\n+\n             log.init.debug(\"Startup cmd {!r}\".format(cmd))\n-            commandrunner = runners.CommandRunner(win_id)\n+            commandrunner = runners.CommandRunner(window.win_id)\n             commandrunner.run_safely(cmd[1:])\n         elif not cmd:\n             log.init.debug(\"Empty argument\")\n-            win_id = mainwindow.get_window(via_ipc=via_ipc,\n-                                           target=new_window_target)\n+            window = mainwindow.get_window(via_ipc=via_ipc, target=new_window_target)\n         else:\n             if via_ipc and target_arg and target_arg != 'auto':\n                 open_target = target_arg\n@@ -291,7 +270,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):\n                 message.error(\"Error in startup argument '{}': {}\".format(\n                     cmd, e))\n             else:\n-                win_id = open_url(url, target=open_target, via_ipc=via_ipc)\n+                window = open_url(url, target=open_target, via_ipc=via_ipc)\n \n \n def open_url(url, target=None, no_raise=False, via_ipc=True):\n@@ -304,39 +283,37 @@ def open_url(url, target=None, no_raise=False, via_ipc=True):\n         via_ipc: Whether the arguments were transmitted over IPC.\n \n     Return:\n-        ID of a window that was used to open URL\n+        The MainWindow of a window that was used to open the URL.\n     \"\"\"\n     target = target or config.val.new_instance_open_target\n     background = target in {'tab-bg', 'tab-bg-silent'}\n-    win_id = mainwindow.get_window(via_ipc=via_ipc, target=target,\n-                                   no_raise=no_raise)\n-    tabbed_browser = objreg.get('tabbed-browser', scope='window',\n-                                window=win_id)\n+    window = mainwindow.get_window(via_ipc=via_ipc, target=target, no_raise=no_raise)\n     log.init.debug(\"About to open URL: {}\".format(url.toDisplayString()))\n-    tabbed_browser.tabopen(url, background=background, related=False)\n-    return win_id\n+    window.tabbed_browser.tabopen(url, background=background, related=False)\n+    window.show()\n+    window.maybe_raise()\n+    return window\n \n \n-def _open_startpage(win_id=None):\n+def _open_startpage(window: Optional[mainwindow.MainWindow] = None) -&gt; None:\n     \"\"\"Open startpage.\n \n     The startpage is never opened if the given windows are not empty.\n \n     Args:\n-        win_id: If None, open startpage in all empty windows.\n+        window: If None, open startpage in all empty windows.\n                 If set, open the startpage in the given window.\n     \"\"\"\n-    if win_id is not None:\n-        window_ids: Iterable[int] = [win_id]\n+    if window is not None:\n+        windows: Iterable[mainwindow.MainWindow] = [window]\n     else:\n-        window_ids = objreg.window_registry\n-    for cur_win_id in list(window_ids):  # Copying as the dict could change\n-        tabbed_browser = objreg.get('tabbed-browser', scope='window',\n-                                    window=cur_win_id)\n-        if tabbed_browser.widget.count() == 0:\n+        windows = objreg.window_registry.values()\n+\n+    for cur_window in list(windows):  # Copying as the dict could change\n+        if cur_window.tabbed_browser.widget.count() == 0:\n             log.init.debug(\"Opening start pages\")\n             for url in config.val.url.start_pages:\n-                tabbed_browser.tabopen(url)\n+                cur_window.tabbed_browser.tabopen(url)\n \n \n def _open_special_pages(args):\n@@ -353,7 +330,7 @@ def _open_special_pages(args):\n     tabbed_browser = objreg.get('tabbed-browser', scope='window',\n                                 window='last-focused')\n \n-    pages = [\n+    pages: List[Tuple[str, bool, str]] = [\n         # state, condition, URL\n         ('quickstart-done',\n          True,\n@@ -369,8 +346,20 @@ def _open_special_pages(args):\n          'qute://warning/webkit'),\n \n         ('session-warning-shown',\n-         qtutils.version_check('5.15', compiled=False),\n+         True,\n          'qute://warning/sessions'),\n+\n+        ('qt5-warning-shown',\n+         (\n+             machinery.IS_QT5 and\n+             machinery.INFO.reason == machinery.SelectionReason.auto and\n+             objects.backend != usertypes.Backend.QtWebKit\n+         ),\n+         'qute://warning/qt5'),\n+\n+        ('birthday-2023-shown',\n+         datetime.date.today() == datetime.date(2023, 12, 14),\n+         'https://qutebrowser.org/birthday.html'),\n     ]\n \n     if 'quickstart-done' not in general_sect:\n@@ -432,10 +421,10 @@ def on_focus_changed(_old, new):\n def open_desktopservices_url(url):\n     \"\"\"Handler to open a URL via QDesktopServices.\"\"\"\n     target = config.val.new_instance_open_target\n-    win_id = mainwindow.get_window(via_ipc=True, target=target)\n-    tabbed_browser = objreg.get('tabbed-browser', scope='window',\n-                                window=win_id)\n-    tabbed_browser.tabopen(url)\n+    window = mainwindow.get_window(via_ipc=True, target=target)\n+    window.tabbed_browser.tabopen(url)\n+    window.show()\n+    window.maybe_raise()\n \n \n # This is effectively a @config.change_filter\n@@ -525,6 +514,7 @@ def _init_modules(*, args):\n     log.init.debug(\"Misc initialization...\")\n     macros.init()\n     windowundo.init()\n+    nativeeventfilter.init()\n \n \n class Application(QApplication):\n@@ -564,11 +554,9 @@ class Application(QApplication):\n         self.launch_time = datetime.datetime.now()\n         self.focusObjectChanged.connect(self.on_focus_object_changed)\n \n-        try:\n-            self.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True)\n-        except AttributeError:\n+        if machinery.IS_QT5:\n             # default and removed in Qt 6\n-            pass\n+            self.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True)\n \n         self.new_window.connect(self._on_new_window)\n \n@@ -580,7 +568,7 @@ class Application(QApplication):\n     @pyqtSlot(QObject)\n     def on_focus_object_changed(self, obj):\n         \"\"\"Log when the focus object changed.\"\"\"\n-        output = repr(obj)\n+        output = qtutils.qobj_repr(obj)\n         if self._last_focus_object != output:\n             log.misc.debug(\"Focus object changed: {}\".format(output))\n         self._last_focus_object = output\ndiff --git a/qutebrowser/browser/__init__.py b/qutebrowser/browser/__init__.py\nindex 340069bcf..acdd9b4a9 100644\n--- a/qutebrowser/browser/__init__.py\n+++ b/qutebrowser/browser/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Classes related to the browser widgets.\"\"\"\ndiff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py\nindex f5ad19437..625046a9c 100644\n--- a/qutebrowser/browser/browsertab.py\n+++ b/qutebrowser/browser/browsertab.py\n@@ -1,48 +1,35 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n-\"\"\"Base class for a wrapper over QWebView/QWebEngineView.\"\"\"\n+\"\"\"Base class for a wrapper over WebView/WebEngineView.\"\"\"\n \n import enum\n+import pathlib\n import itertools\n import functools\n import dataclasses\n from typing import (cast, TYPE_CHECKING, Any, Callable, Iterable, List, Optional,\n                     Sequence, Set, Type, Union, Tuple)\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt,\n-                          QEvent, QPoint, QRect)\n+                          QEvent, QPoint, QRect, QTimer)\n from qutebrowser.qt.gui import QKeyEvent, QIcon, QPixmap\n-from qutebrowser.qt.widgets import QWidget, QApplication, QDialog\n+from qutebrowser.qt.widgets import QApplication, QWidget\n from qutebrowser.qt.printsupport import QPrintDialog, QPrinter\n from qutebrowser.qt.network import QNetworkAccessManager\n \n if TYPE_CHECKING:\n     from qutebrowser.qt.webkit import QWebHistory, QWebHistoryItem\n-    from qutebrowser.qt.webkitwidgets import QWebPage, QWebView\n-    from qutebrowser.qt.webenginewidgets import (\n-        QWebEngineHistory, QWebEngineHistoryItem, QWebEnginePage, QWebEngineView)\n+    from qutebrowser.qt.webkitwidgets import QWebPage\n+    from qutebrowser.qt.webenginecore import (\n+        QWebEngineHistory, QWebEngineHistoryItem, QWebEnginePage)\n \n from qutebrowser.keyinput import modeman\n from qutebrowser.config import config, websettings\n from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils,\n-                               urlutils, message, jinja)\n+                               urlutils, message, jinja, version)\n from qutebrowser.misc import miscwidgets, objects, sessions\n from qutebrowser.browser import eventfilter, inspector\n from qutebrowser.qt import sip\n@@ -50,10 +37,12 @@ from qutebrowser.qt import sip\n if TYPE_CHECKING:\n     from qutebrowser.browser import webelem\n     from qutebrowser.browser.inspector import AbstractWebInspector\n+    from qutebrowser.browser.webengine.webview import WebEngineView\n+    from qutebrowser.browser.webkit.webview import WebView\n \n \n tab_id_gen = itertools.count(0)\n-_WidgetType = Union[\"QWebView\", \"QWebEngineView\"]\n+_WidgetType = Union[\"WebView\", \"WebEngineView\"]\n \n \n def create(win_id: int,\n@@ -153,7 +142,6 @@ class AbstractAction:\n \n     \"\"\"Attribute ``action`` of AbstractTab for Qt WebActions.\"\"\"\n \n-    action_class: Type[Union['QWebPage', 'QWebEnginePage']]\n     action_base: Type[Union['QWebPage.WebAction', 'QWebEnginePage.WebAction']]\n \n     def __init__(self, tab: 'AbstractTab') -&gt; None:\n@@ -170,10 +158,10 @@ class AbstractAction:\n \n     def run_string(self, name: str) -&gt; None:\n         \"\"\"Run a webaction based on its name.\"\"\"\n-        member = getattr(self.action_class, name, None)\n-        if not isinstance(member, self.action_base):\n-            raise WebTabError(\"{} is not a valid web action!\".format(name))\n-        assert member is not None  # for mypy\n+        try:\n+            member = getattr(self.action_base, name)\n+        except AttributeError:\n+            raise WebTabError(f\"{name} is not a valid web action!\")\n         self._widget.triggerPageAction(member)\n \n     def show_source(self, pygments: bool = False) -&gt; None:\n@@ -226,13 +214,37 @@ class AbstractAction:\n         self._tab.dump_async(show_source_cb)\n \n \n-class AbstractPrinting:\n+class AbstractPrinting(QObject):\n \n     \"\"\"Attribute ``printing`` of AbstractTab for printing the page.\"\"\"\n \n-    def __init__(self, tab: 'AbstractTab') -&gt; None:\n+    printing_finished = pyqtSignal(bool)\n+    pdf_printing_finished = pyqtSignal(str, bool)  # filename, ok\n+\n+    def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -&gt; None:\n+        super().__init__(parent)\n         self._widget = cast(_WidgetType, None)\n         self._tab = tab\n+        self._dialog: Optional[QPrintDialog] = None\n+        self.printing_finished.connect(self._on_printing_finished)\n+        self.pdf_printing_finished.connect(self._on_pdf_printing_finished)\n+\n+    @pyqtSlot(bool)\n+    def _on_printing_finished(self, ok: bool) -&gt; None:\n+        # Only reporting error here, as the user has feedback from the dialog\n+        # (and probably their printer) already.\n+        if not ok:\n+            message.error(\"Printing failed!\")\n+        if self._dialog is not None:\n+            self._dialog.deleteLater()\n+            self._dialog = None\n+\n+    @pyqtSlot(str, bool)\n+    def _on_pdf_printing_finished(self, path: str, ok: bool) -&gt; None:\n+        if ok:\n+            message.info(f\"Printed to {path}\")\n+        else:\n+            message.error(f\"Printing to {path} failed!\")\n \n     def check_pdf_support(self) -&gt; None:\n         \"\"\"Check whether writing to PDFs is supported.\n@@ -250,41 +262,29 @@ class AbstractPrinting:\n         \"\"\"\n         raise NotImplementedError\n \n-    def to_pdf(self, filename: str) -&gt; bool:\n+    def to_pdf(self, path: pathlib.Path) -&gt; None:\n         \"\"\"Print the tab to a PDF with the given filename.\"\"\"\n         raise NotImplementedError\n \n-    def to_printer(self, printer: QPrinter,\n-                   callback: Callable[[bool], None] = None) -&gt; None:\n+    def to_printer(self, printer: QPrinter) -&gt; None:\n         \"\"\"Print the tab.\n \n         Args:\n             printer: The QPrinter to print to.\n-            callback: Called with a boolean\n-                      (True if printing succeeded, False otherwise)\n         \"\"\"\n         raise NotImplementedError\n \n+    def _do_print(self) -&gt; None:\n+        assert self._dialog is not None\n+        printer = self._dialog.printer()\n+        assert printer is not None\n+        self.to_printer(printer)\n+\n     def show_dialog(self) -&gt; None:\n         \"\"\"Print with a QPrintDialog.\"\"\"\n-        def print_callback(ok: bool) -&gt; None:\n-            \"\"\"Called when printing finished.\"\"\"\n-            if not ok:\n-                message.error(\"Printing failed!\")\n-            diag.deleteLater()\n-\n-        def do_print() -&gt; None:\n-            \"\"\"Called when the dialog was closed.\"\"\"\n-            self.to_printer(diag.printer(), print_callback)\n-\n-        diag = QPrintDialog(self._tab)\n-        if utils.is_mac:\n-            # For some reason we get a segfault when using open() on macOS\n-            ret = diag.exec()\n-            if ret == QDialog.DialogCode.Accepted:\n-                do_print()\n-        else:\n-            diag.open(do_print)\n+        self._dialog = QPrintDialog(self._tab)\n+        self._dialog.open(self._do_print)\n+        # Gets cleaned up in on_printing_finished\n \n \n @dataclasses.dataclass\n@@ -490,7 +490,7 @@ class AbstractZoom(QObject):\n             raise ValueError(\"Can't zoom to factor {}!\".format(factor))\n \n         default_zoom_factor = float(config.val.zoom.default) / 100\n-        self._default_zoom_changed = (factor != default_zoom_factor)\n+        self._default_zoom_changed = factor != default_zoom_factor\n \n         self._zoom_factor = factor\n         self._set_factor_internal(factor)\n@@ -902,7 +902,13 @@ class AbstractTabPrivate:\n                 modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,\n                               'load finished', only_if_normal=True)\n \n-        self._tab.elements.find_focused(_auto_insert_mode_cb)\n+        # There seems to be a race between loadFinished being called,\n+        # and the autoload attribute on websites actually focusing anything.\n+        # Thus, we delay this by a bit. Locally, a delay of 13ms caused no races\n+        # with 5000 test reruns (even with simultaneous CPU stress testing),\n+        # so 65ms should be a safe bet and still not be too noticeable.\n+        QTimer.singleShot(\n+            65, lambda: self._tab.elements.find_focused(_auto_insert_mode_cb))\n \n     def clear_ssl_errors(self) -&gt; None:\n         raise NotImplementedError\n@@ -965,7 +971,7 @@ class AbstractTabPrivate:\n \n class AbstractTab(QWidget):\n \n-    \"\"\"An adapter for QWebView/QWebEngineView representing a single tab.\"\"\"\n+    \"\"\"An adapter for WebView/WebEngineView representing a single tab.\"\"\"\n \n     #: Signal emitted when a website requests to close this tab.\n     window_close_requested = pyqtSignal()\n@@ -1059,11 +1065,14 @@ class AbstractTab(QWidget):\n \n         self.before_load_started.connect(self._on_before_load_started)\n \n-    def _set_widget(self, widget: Union[\"QWebView\", \"QWebEngineView\"]) -&gt; None:\n+    def _set_widget(self, widget: _WidgetType) -&gt; None:\n         # pylint: disable=protected-access\n         self._widget = widget\n+        # FIXME:v4 ignore needed for QtWebKit\n         self.data.splitter = miscwidgets.InspectorSplitter(\n-            win_id=self.win_id, main_webview=widget)\n+            win_id=self.win_id,\n+            main_webview=widget,  # type: ignore[arg-type,unused-ignore]\n+        )\n         self._layout.wrap(self, self.data.splitter)\n         self.history._history = widget.history()\n         self.history.private_api._history = widget.history()\n@@ -1146,10 +1155,11 @@ class AbstractTab(QWidget):\n     ) -&gt; None:\n         \"\"\"Handle common acceptNavigationRequest code.\"\"\"\n         url = utils.elide(navigation.url.toDisplayString(), 100)\n-        log.webview.debug(\"navigation request: url {}, type {}, is_main_frame \"\n-                          \"{}\".format(url,\n-                                      navigation.navigation_type,\n-                                      navigation.is_main_frame))\n+        log.webview.debug(\n+            f\"navigation request: url {url} (current {self.url().toDisplayString()}), \"\n+            f\"type {navigation.navigation_type.name}, \"\n+            f\"is_main_frame {navigation.is_main_frame}\"\n+        )\n \n         if navigation.is_main_frame:\n             self.data.last_navigation = navigation\n@@ -1167,10 +1177,41 @@ class AbstractTab(QWidget):\n                                   navigation.url.errorString()))\n             navigation.accepted = False\n \n+        # WORKAROUND for QtWebEngine &gt;= 6.2 not allowing form requests from\n+        # qute:// to outside domains.\n+        needs_load_workarounds = (\n+            objects.backend == usertypes.Backend.QtWebEngine and\n+            version.qtwebengine_versions().webengine &gt;= utils.VersionNumber(6, 2)\n+        )\n+        if (\n+            needs_load_workarounds and\n+            self.url() == QUrl(\"qute://start/\") and\n+            navigation.navigation_type == navigation.Type.form_submitted and\n+            navigation.url.matches(\n+                QUrl(config.val.url.searchengines['DEFAULT']),\n+                urlutils.FormatOption.REMOVE_QUERY)\n+        ):\n+            log.webview.debug(\n+                \"Working around qute://start loading issue for \"\n+                f\"{navigation.url.toDisplayString()}\")\n+            navigation.accepted = False\n+            self.load_url(navigation.url)\n+\n+        if (\n+            needs_load_workarounds and\n+            self.url() == QUrl(\"qute://bookmarks/\") and\n+            navigation.navigation_type == navigation.Type.back_forward\n+        ):\n+            log.webview.debug(\n+                \"Working around qute://bookmarks loading issue for \"\n+                f\"{navigation.url.toDisplayString()}\")\n+            navigation.accepted = False\n+            self.load_url(navigation.url)\n+\n     @pyqtSlot(bool)\n     def _on_load_finished(self, ok: bool) -&gt; None:\n         assert self._widget is not None\n-        if sip.isdeleted(self._widget):\n+        if self.is_deleted():\n             # https://github.com/qutebrowser/qutebrowser/issues/3498\n             return\n \n@@ -1305,18 +1346,22 @@ class AbstractTab(QWidget):\n             pic = self._widget.grab()\n         else:\n             qtutils.ensure_valid(rect)\n-            pic = self._widget.grab(rect)\n+            # FIXME:v4 ignore needed for QtWebKit\n+            pic = self._widget.grab(rect)  # type: ignore[arg-type,unused-ignore]\n \n         if pic.isNull():\n             return None\n \n+        if machinery.IS_QT6:\n+            # FIXME:v4 cast needed for QtWebKit\n+            pic = cast(QPixmap, pic)\n+\n         return pic\n \n     def __repr__(self) -&gt; str:\n         try:\n             qurl = self.url()\n-            url = qurl.toDisplayString(\n-                QUrl.ComponentFormattingOptions.EncodeUnicode)  # type: ignore[arg-type]\n+            url = qurl.toDisplayString(urlutils.FormatOption.ENCODE_UNICODE)\n         except (AttributeError, RuntimeError) as exc:\n             url = '&lt;{}&gt;'.format(exc.__class__.__name__)\n         else:\n@@ -1324,5 +1369,11 @@ class AbstractTab(QWidget):\n         return utils.get_repr(self, tab_id=self.tab_id, url=url)\n \n     def is_deleted(self) -&gt; bool:\n+        \"\"\"Check if the tab has been deleted.\"\"\"\n         assert self._widget is not None\n-        return sip.isdeleted(self._widget)\n+        # FIXME:v4 cast needed for QtWebKit\n+        if machinery.IS_QT6:\n+            widget = cast(QWidget, self._widget)\n+        else:\n+            widget = self._widget\n+        return sip.isdeleted(widget)\ndiff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py\nindex 5b431f8e7..06298a8ca 100644\n--- a/qutebrowser/browser/commands.py\n+++ b/qutebrowser/browser/commands.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Command dispatcher for TabbedBrowser.\"\"\"\n \n@@ -70,9 +55,7 @@ class CommandDispatcher:\n             raise cmdutils.CommandError(\"Private windows are unavailable with \"\n                                         \"the single-process process model.\")\n \n-        new_window = mainwindow.MainWindow(private=private)\n-        new_window.show()\n-        return new_window.tabbed_browser\n+        return mainwindow.MainWindow(private=private).tabbed_browser\n \n     def _count(self) -&gt; int:\n         \"\"\"Convenience method to get the widget count.\"\"\"\n@@ -109,8 +92,15 @@ class CommandDispatcher:\n             raise cmdutils.CommandError(\"No WebView available yet!\")\n         return widget\n \n-    def _open(self, url, tab=False, background=False, window=False,\n-              related=False, private=None):\n+    def _open(\n+        self,\n+        url: QUrl,\n+        tab: bool = False,\n+        background: bool = False,\n+        window: bool = False,\n+        related: bool = False,\n+        private: Optional[bool] = None,\n+    ) -&gt; None:\n         \"\"\"Helper function to open a page.\n \n         Args:\n@@ -123,13 +113,15 @@ class CommandDispatcher:\n         \"\"\"\n         urlutils.raise_cmdexc_if_invalid(url)\n         tabbed_browser = self._tabbed_browser\n-        cmdutils.check_exclusive((tab, background, window, private), 'tbwp')\n+        cmdutils.check_exclusive((tab, background, window, private or False), 'tbwp')\n         if window and private is None:\n             private = self._tabbed_browser.is_private\n \n         if window or private:\n+            assert isinstance(private, bool)\n             tabbed_browser = self._new_tabbed_browser(private)\n             tabbed_browser.tabopen(url)\n+            tabbed_browser.window().show()\n         elif tab:\n             tabbed_browser.tabopen(url, background=False, related=related)\n         elif background:\n@@ -403,14 +395,17 @@ class CommandDispatcher:\n         except browsertab.WebTabError as e:\n             raise cmdutils.CommandError(e)\n \n-        # The new tab could be in a new tabbed_browser (e.g. because of\n-        # tabs.tabs_are_windows being set)\n         if window or private:\n             new_tabbed_browser = self._new_tabbed_browser(\n                 private=self._tabbed_browser.is_private or private)\n         else:\n             new_tabbed_browser = self._tabbed_browser\n+\n         newtab = new_tabbed_browser.tabopen(background=bg)\n+        new_tabbed_browser.window().show()\n+\n+        # The new tab could be in a new tabbed_browser (e.g. because of\n+        # tabs.tabs_are_windows being set)\n         new_tabbed_browser = objreg.get('tabbed-browser', scope='window',\n                                         window=newtab.win_id)\n         idx = new_tabbed_browser.widget.indexOf(newtab)\n@@ -498,6 +493,8 @@ class CommandDispatcher:\n                     \"The window with id {} is not private\".format(win_id))\n \n         tabbed_browser.tabopen(self._current_url())\n+        tabbed_browser.window().show()\n+\n         if not keep:\n             self._tabbed_browser.close_tab(self._current_widget(),\n                                            add_undo=False,\n@@ -706,9 +703,10 @@ class CommandDispatcher:\n         assert what in ['url', 'pretty-url'], what\n \n         if what == 'pretty-url':\n-            flags = QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.DecodeReserved\n+            flags = urlutils.FormatOption.DECODE_RESERVED\n         else:\n-            flags = QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded\n+            flags = urlutils.FormatOption.ENCODED\n+        flags |= urlutils.FormatOption.REMOVE_PASSWORD\n \n         url = QUrl(self._current_url())\n         url_query = QUrlQuery()\n@@ -720,7 +718,7 @@ class CommandDispatcher:\n             if key in config.val.url.yank_ignored_parameters:\n                 url_query.removeQueryItem(key)\n         url.setQuery(url_query)\n-        return url.toString(flags)  # type: ignore[arg-type]\n+        return url.toString(flags)\n \n     @cmdutils.register(instance='command-dispatcher', scope='window')\n     @cmdutils.argument('what', choices=['selection', 'url', 'pretty-url',\n@@ -950,6 +948,8 @@ class CommandDispatcher:\n                     \"No window specified and couldn't find active window!\")\n             assert isinstance(active_win, mainwindow.MainWindow), active_win\n             win_id = active_win.win_id\n+        else:\n+            raise utils.Unreachable(index_parts)\n \n         if win_id not in objreg.window_registry:\n             raise cmdutils.CommandError(\n@@ -1191,8 +1191,9 @@ class CommandDispatcher:\n             env['QUTE_TAB_INDEX'] = str(idx + 1)\n             env['QUTE_TITLE'] = self._tabbed_browser.widget.page_title(idx)\n \n-        # FIXME:qtwebengine: If tab is None, run_async will fail!\n         tab = self._tabbed_browser.widget.currentWidget()\n+        if tab is None:\n+            raise cmdutils.CommandError(\"No current tab!\")\n \n         try:\n             url = self._tabbed_browser.current_url()\n@@ -1236,21 +1237,31 @@ class CommandDispatcher:\n     @cmdutils.register(instance='command-dispatcher', scope='window',\n                        maxsplit=0)\n     @cmdutils.argument('name', completion=miscmodels.quickmark)\n-    def quickmark_del(self, name=None):\n+    def quickmark_del(self, name=None, all_=False):\n         \"\"\"Delete a quickmark.\n \n         Args:\n             name: The name of the quickmark to delete. If not given, delete the\n                   quickmark for the current page (choosing one arbitrarily\n                   if there are more than one).\n+            all_: Delete all quickmarks.\n         \"\"\"\n         quickmark_manager = objreg.get('quickmark-manager')\n+\n+        if all_:\n+            if name is not None:\n+                raise cmdutils.CommandError(\"Cannot specify name and --all\")\n+            quickmark_manager.clear()\n+            message.info(\"Quickmarks cleared.\")\n+            return\n+\n         if name is None:\n             url = self._current_url()\n             try:\n                 name = quickmark_manager.get_by_qurl(url)\n             except urlmarks.DoesNotExistError as e:\n                 raise cmdutils.CommandError(str(e))\n+\n         try:\n             quickmark_manager.delete(name)\n         except KeyError:\n@@ -1293,9 +1304,8 @@ class CommandDispatcher:\n             was_added = bookmark_manager.add(url, title, toggle=toggle)\n         except urlmarks.Error as e:\n             raise cmdutils.CommandError(str(e))\n-        else:\n-            msg = \"Bookmarked {}\" if was_added else \"Removed bookmark {}\"\n-            message.info(msg.format(url.toDisplayString()))\n+        msg = \"Bookmarked {}\" if was_added else \"Removed bookmark {}\"\n+        message.info(msg.format(url.toDisplayString()))\n \n     @cmdutils.register(instance='command-dispatcher', scope='window',\n                        maxsplit=0)\n@@ -1322,18 +1332,28 @@ class CommandDispatcher:\n     @cmdutils.register(instance='command-dispatcher', scope='window',\n                        maxsplit=0)\n     @cmdutils.argument('url', completion=miscmodels.bookmark)\n-    def bookmark_del(self, url=None):\n+    def bookmark_del(self, url=None, all_=False):\n         \"\"\"Delete a bookmark.\n \n         Args:\n             url: The url of the bookmark to delete. If not given, use the\n                  current page's url.\n+            all_: If given, delete all bookmarks.\n         \"\"\"\n+        bookmark_manager = objreg.get('bookmark-manager')\n+        if all_:\n+            if url is not None:\n+                raise cmdutils.CommandError(\"Cannot specify url and --all\")\n+            bookmark_manager.clear()\n+            message.info(\"Bookmarks cleared.\")\n+            return\n+\n         if url is None:\n             url = self._current_url().toString(QUrl.UrlFormattingOption.RemovePassword |\n                                                QUrl.ComponentFormattingOption.FullyEncoded)\n+\n         try:\n-            objreg.get('bookmark-manager').delete(url)\n+            bookmark_manager.delete(url)\n         except KeyError:\n             raise cmdutils.CommandError(\"Bookmark '{}' not found!\".format(url))\n         message.info(\"Removed bookmark {}\".format(url))\n@@ -1586,8 +1606,7 @@ class CommandDispatcher:\n     def _search_navigation_cb(self, result):\n         \"\"\"Callback called from :search-prev/next.\"\"\"\n         if result == browsertab.SearchNavigationResult.not_found:\n-            # FIXME check if this actually can happen...\n-            message.warning(\"Search result vanished...\")\n+            self._search_cb(found=False, text=self._tabbed_browser.search_text)\n             return\n         elif result == browsertab.SearchNavigationResult.found:\n             return\ndiff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py\nindex 8ce0ca1b2..28f20b0ef 100644\n--- a/qutebrowser/browser/downloads.py\n+++ b/qutebrowser/browser/downloads.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Shared QtWebKit/QtWebEngine code for downloads.\"\"\"\n \n@@ -832,8 +817,13 @@ class AbstractDownloadItem(QObject):\n         - file:// downloads from file:// URLs (open the file instead)\n         - http:// downloads from https:// URLs (mixed content)\n         \"\"\"\n-        origin = self.origin()\n         url = self.url()\n+        if url.scheme() not in [\"file\", \"http\"]:\n+            # WORKAROUND to avoid calling self.origin() if unneeded:\n+            # https://github.com/qutebrowser/qutebrowser/issues/7854\n+            return False\n+\n+        origin = self.origin()\n         if not origin.isValid():\n             return False\n \n@@ -843,7 +833,7 @@ class AbstractDownloadItem(QObject):\n             return True\n \n         if (url.scheme() == \"http\" and\n-                origin.isValid() and origin.scheme() == \"https\" and\n+                origin.scheme() == \"https\" and\n                 config.instance.get(\"downloads.prevent_mixed_content\", url=origin)):\n             self._die(\"Aborting insecure download from secure page \"\n                       \"(see downloads.prevent_mixed_content).\")\ndiff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py\nindex da0763b76..4b6a8b2c8 100644\n--- a/qutebrowser/browser/downloadview.py\n+++ b/qutebrowser/browser/downloadview.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The ListView to display downloads in.\"\"\"\n \n@@ -50,6 +35,7 @@ class DownloadView(QListView):\n         QListView {\n             background-color: {{ conf.colors.downloads.bar.bg }};\n             font: {{ conf.fonts.downloads }};\n+            border: 0;\n         }\n \n         QListView::item {\n@@ -79,9 +65,10 @@ class DownloadView(QListView):\n         self.clicked.connect(self.on_clicked)\n \n     def __repr__(self):\n-        model = self.model()\n+        model = qtutils.add_optional(self.model())\n+        count: Union[int, str]\n         if model is None:\n-            count = 'None'  # type: ignore[unreachable]\n+            count = 'None'\n         else:\n             count = model.rowCount()\n         return utils.get_repr(self, count=count)\n@@ -175,9 +162,12 @@ class DownloadView(QListView):\n                 assert name is not None\n                 assert handler is not None\n                 action = self._menu.addAction(name)\n+                assert action is not None\n                 action.triggered.connect(handler)\n         if actions:\n-            self._menu.popup(self.viewport().mapToGlobal(point))\n+            viewport = self.viewport()\n+            assert viewport is not None\n+            self._menu.popup(viewport.mapToGlobal(point))\n \n     def minimumSizeHint(self):\n         \"\"\"Override minimumSizeHint so the size is correct in a layout.\"\"\"\ndiff --git a/qutebrowser/browser/eventfilter.py b/qutebrowser/browser/eventfilter.py\nindex 6c27cb9f6..1cff11ac4 100644\n--- a/qutebrowser/browser/eventfilter.py\n+++ b/qutebrowser/browser/eventfilter.py\n@@ -1,29 +1,15 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Event handling for a browser tab.\"\"\"\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import QObject, QEvent, Qt, QTimer\n+from qutebrowser.qt.widgets import QWidget\n \n from qutebrowser.config import config\n-from qutebrowser.utils import message, log, usertypes, qtutils\n-from qutebrowser.misc import objects\n+from qutebrowser.utils import log, message, usertypes, qtutils\n from qutebrowser.keyinput import modeman\n \n \n@@ -50,17 +36,42 @@ class ChildEventFilter(QObject):\n         \"\"\"Act on ChildAdded events.\"\"\"\n         if event.type() == QEvent.Type.ChildAdded:\n             child = event.child()\n-            log.misc.debug(\"{} got new child {}, installing filter\"\n-                           .format(obj, child))\n+            if not isinstance(child, QWidget):\n+                # Can e.g. happen when dragging text\n+                log.misc.debug(f\"Ignoring new child {qtutils.qobj_repr(child)}\")\n+                return False\n+\n+            log.misc.debug(\n+                f\"{qtutils.qobj_repr(obj)} got new child {qtutils.qobj_repr(child)}, \"\n+                \"installing filter\")\n \n             # Additional sanity check, but optional\n             if self._widget is not None:\n                 assert obj is self._widget\n \n+                # WORKAROUND for unknown Qt bug losing focus on child change\n+                # Carry on keyboard focus to the new child if:\n+                # - This is a child event filter on a tab (self._widget is not None)\n+                # - We find an old existing child which is a QQuickWidget and is\n+                #   currently focused.\n+                # - We're using QtWebEngine &gt;= 6.4 (older versions are not affected)\n+                children = [\n+                    c for c in self._widget.findChildren(\n+                        QWidget, \"\", Qt.FindChildOption.FindDirectChildrenOnly)\n+                    if c is not child and\n+                    c.hasFocus() and\n+                    c.metaObject() is not None and\n+                    c.metaObject().className() == \"QQuickWidget\"\n+                ]\n+                if children:\n+                    log.misc.debug(\"Focusing new child\")\n+                    child.setFocus()\n+\n             child.installEventFilter(self._filter)\n         elif event.type() == QEvent.Type.ChildRemoved:\n             child = event.child()\n-            log.misc.debug(\"{}: removed child {}\".format(obj, child))\n+            log.misc.debug(\n+                f\"{qtutils.qobj_repr(obj)}: removed child {qtutils.qobj_repr(child)}\")\n \n         return False\n \n@@ -84,7 +95,6 @@ class TabEventFilter(QObject):\n             QEvent.Type.MouseButtonPress: self._handle_mouse_press,\n             QEvent.Type.MouseButtonRelease: self._handle_mouse_release,\n             QEvent.Type.Wheel: self._handle_wheel,\n-            QEvent.Type.KeyRelease: self._handle_key_release,\n         }\n         self._ignore_wheel_event = False\n         self._check_insertmode_on_release = False\n@@ -102,7 +112,10 @@ class TabEventFilter(QObject):\n                              e.buttons() == Qt.MouseButton.LeftButton | Qt.MouseButton.RightButton)\n \n         if e.button() in [Qt.MouseButton.XButton1, Qt.MouseButton.XButton2] or is_rocker_gesture:\n-            self._mousepress_backforward(e)\n+            if not machinery.IS_QT6:\n+                self._mousepress_backforward(e)\n+            # FIXME:qt6 For some reason, this doesn't filter the action on\n+            # Qt 6...\n             return True\n \n         self._ignore_wheel_event = True\n@@ -170,21 +183,6 @@ class TabEventFilter(QObject):\n \n         return False\n \n-    def _handle_key_release(self, e):\n-        \"\"\"Ignore repeated key release events going to the website.\n-\n-        WORKAROUND for https://bugreports.qt.io/browse/QTBUG-77208\n-\n-        Args:\n-            e: The QKeyEvent.\n-\n-        Return:\n-            True if the event should be filtered, False otherwise.\n-        \"\"\"\n-        return (e.isAutoRepeat() and\n-                not qtutils.version_check('5.14', compiled=False) and\n-                objects.backend == usertypes.Backend.QtWebEngine)\n-\n     def _mousepress_insertmode_cb(self, elem):\n         \"\"\"Check if the clicked element is editable.\"\"\"\n         if elem is None:\ndiff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py\nindex 62c304bd8..d41d46361 100644\n--- a/qutebrowser/browser/greasemonkey.py\n+++ b/qutebrowser/browser/greasemonkey.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Load, parse and make available Greasemonkey scripts.\"\"\"\n \n@@ -145,7 +130,7 @@ class GreasemonkeyScript:\n     def needs_document_end_workaround(self):\n         \"\"\"Check whether to force @run-at document-end.\n \n-        This needs to be done on QtWebEngine (since Qt 5.12) for known-broken scripts.\n+        This needs to be done on QtWebEngine for known-broken scripts.\n \n         On Qt 5.12, accessing the DOM isn't possible with \"@run-at\n         document-start\". It was documented to be impossible before, but seems\ndiff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py\nindex 91534a58b..e32567e4d 100644\n--- a/qutebrowser/browser/hints.py\n+++ b/qutebrowser/browser/hints.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"A HintManager to draw hints over links.\"\"\"\n \n@@ -38,7 +23,7 @@ from qutebrowser.keyinput import modeman, modeparsers, basekeyparser\n from qutebrowser.browser import webelem, history\n from qutebrowser.commands import runners\n from qutebrowser.api import cmdutils\n-from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils\n+from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils, urlutils\n if TYPE_CHECKING:\n     from qutebrowser.browser import browsertab\n \n@@ -252,9 +237,9 @@ class HintActions:\n         sel = (context.target == Target.yank_primary and\n                utils.supports_selection())\n \n-        flags = QUrl.ComponentFormattingOption.FullyEncoded | QUrl.UrlFormattingOption.RemovePassword\n+        flags = urlutils.FormatOption.ENCODED | urlutils.FormatOption.REMOVE_PASSWORD\n         if url.scheme() == 'mailto':\n-            flags |= QUrl.UrlFormattingOption.RemoveScheme\n+            flags |= urlutils.FormatOption.REMOVE_SCHEME\n         urlstr = url.toString(flags)\n \n         new_content = urlstr\n@@ -276,22 +261,21 @@ class HintActions:\n \n     def run_cmd(self, url: QUrl, context: HintContext) -&gt; None:\n         \"\"\"Run the command based on a hint URL.\"\"\"\n-        urlstr = url.toString(QUrl.ComponentFormattingOption.FullyEncoded)  # type: ignore[arg-type]\n+        urlstr = url.toString(urlutils.FormatOption.ENCODED)\n         args = context.get_args(urlstr)\n         commandrunner = runners.CommandRunner(self._win_id)\n         commandrunner.run_safely(' '.join(args))\n \n     def preset_cmd_text(self, url: QUrl, context: HintContext) -&gt; None:\n         \"\"\"Preset a commandline text based on a hint URL.\"\"\"\n-        flags = QUrl.ComponentFormattingOption.FullyEncoded\n-        urlstr = url.toDisplayString(flags)  # type: ignore[arg-type]\n+        urlstr = url.toDisplayString(urlutils.FormatOption.ENCODED)\n         args = context.get_args(urlstr)\n         text = ' '.join(args)\n         if text[0] not in modeparsers.STARTCHARS:\n             raise HintingError(\"Invalid command text '{}'.\".format(text))\n \n         cmd = objreg.get('status-command', scope='window', window=self._win_id)\n-        cmd.set_cmd_text(text)\n+        cmd.cmd_set_text(text)\n \n     def download(self, elem: webelem.AbstractWebElement,\n                  context: HintContext) -&gt; None:\n@@ -325,19 +309,18 @@ class HintActions:\n \n         cmd = context.args[0]\n         args = context.args[1:]\n-        flags = QUrl.ComponentFormattingOption.FullyEncoded\n+        flags = urlutils.FormatOption.ENCODED\n \n         env = {\n             'QUTE_MODE': 'hints',\n             'QUTE_SELECTED_TEXT': str(elem),\n             'QUTE_SELECTED_HTML': elem.outer_xml(),\n-            'QUTE_CURRENT_URL':\n-                context.baseurl.toString(flags),  # type: ignore[arg-type]\n+            'QUTE_CURRENT_URL': context.baseurl.toString(flags),\n         }\n \n         url = elem.resolve_url(context.baseurl)\n         if url is not None:\n-            env['QUTE_URL'] = url.toString(flags)  # type: ignore[arg-type]\n+            env['QUTE_URL'] = url.toString(flags)\n \n         try:\n             userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,\ndiff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py\nindex 312edfc13..45bfeddbf 100644\n--- a/qutebrowser/browser/history.py\n+++ b/qutebrowser/browser/history.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Simple history which gets written to disk.\"\"\"\n \n@@ -25,8 +10,9 @@ import contextlib\n import pathlib\n from typing import cast, Mapping, MutableSequence, Optional\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import pyqtSlot, QUrl, QObject, pyqtSignal\n-from qutebrowser.qt.widgets import QProgressDialog, QApplication\n+from qutebrowser.qt.widgets import QProgressDialog, QApplication, QPushButton\n \n from qutebrowser.config import config\n from qutebrowser.api import cmdutils\n@@ -56,7 +42,13 @@ class HistoryProgress:\n         self._progress.setMaximum(0)  # unknown\n         self._progress.setMinimumDuration(0)\n         self._progress.setLabelText(text)\n-        self._progress.setCancelButton(None)\n+\n+        no_button = None\n+        if machinery.IS_QT6:\n+            # FIXME:mypy PyQt6 stubs issue\n+            no_button = cast(QPushButton, None)\n+\n+        self._progress.setCancelButton(no_button)\n         self._progress.setAutoClose(False)\n         self._progress.show()\n         QApplication.processEvents()\n@@ -217,19 +209,19 @@ class WebHistory(sql.SqlTable):\n         self.create_index('HistoryIndex', 'url')\n         self.create_index('HistoryAtimeIndex', 'atime')\n         self._contains_query = self.contains_query('url')\n-        self._between_query = self.database.query('SELECT * FROM History '\n-                                                  'where not redirect '\n-                                                  'and not url like \"qute://%\" '\n-                                                  'and atime &gt; :earliest '\n-                                                  'and atime &lt;= :latest '\n-                                                  'ORDER BY atime desc')\n-\n-        self._before_query = self.database.query('SELECT * FROM History '\n-                                                 'where not redirect '\n-                                                 'and not url like \"qute://%\" '\n-                                                 'and atime &lt;= :latest '\n-                                                 'ORDER BY atime desc '\n-                                                 'limit :limit offset :offset')\n+        self._between_query = self.database.query(\"SELECT * FROM History \"\n+                                                  \"where not redirect \"\n+                                                  \"and not url like 'qute://%' \"\n+                                                  \"and atime &gt; :earliest \"\n+                                                  \"and atime &lt;= :latest \"\n+                                                  \"ORDER BY atime desc\")\n+\n+        self._before_query = self.database.query(\"SELECT * FROM History \"\n+                                                 \"where not redirect \"\n+                                                 \"and not url like 'qute://%' \"\n+                                                 \"and atime &lt;= :latest \"\n+                                                 \"ORDER BY atime desc \"\n+                                                 \"limit :limit offset :offset\")\n \n     def __repr__(self):\n         return utils.get_repr(self, length=len(self))\ndiff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py\nindex dcf718552..e60e4a2b8 100644\n--- a/qutebrowser/browser/inspector.py\n+++ b/qutebrowser/browser/inspector.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Base class for a QtWebKit/QtWebEngine web inspector.\"\"\"\n \n@@ -30,7 +15,7 @@ from qutebrowser.qt.gui import QCloseEvent\n \n from qutebrowser.browser import eventfilter\n from qutebrowser.config import configfiles, config\n-from qutebrowser.utils import log, usertypes\n+from qutebrowser.utils import log, usertypes, qtutils\n from qutebrowser.keyinput import modeman\n from qutebrowser.misc import miscwidgets\n \n@@ -72,8 +57,9 @@ class _EventFilter(QObject):\n \n     clicked = pyqtSignal()\n \n-    def eventFilter(self, _obj: QObject, event: QEvent) -&gt; bool:\n+    def eventFilter(self, _obj: Optional[QObject], event: Optional[QEvent]) -&gt; bool:\n         \"\"\"Translate mouse presses to a clicked signal.\"\"\"\n+        assert event is not None\n         if event.type() == QEvent.Type.MouseButtonPress:\n             self.clicked.emit()\n         return False\n@@ -164,7 +150,7 @@ class AbstractWebInspector(QWidget):\n             self.shutdown()\n             return\n         elif position == Position.window:\n-            self.setParent(None)  # type: ignore[call-overload]\n+            self.setParent(qtutils.QT_NONE)\n             self._load_state_geometry()\n         else:\n             self._splitter.set_inspector(self, position)\n@@ -197,7 +183,7 @@ class AbstractWebInspector(QWidget):\n             if not ok:\n                 log.init.warning(\"Error while loading geometry.\")\n \n-    def closeEvent(self, _e: QCloseEvent) -&gt; None:\n+    def closeEvent(self, _e: Optional[QCloseEvent]) -&gt; None:\n         \"\"\"Save the geometry when closed.\"\"\"\n         data = self._widget.saveGeometry().data()\n         geom = base64.b64encode(data).decode('ASCII')\ndiff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py\nindex d2783e349..e75365bcd 100644\n--- a/qutebrowser/browser/navigate.py\n+++ b/qutebrowser/browser/navigate.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Implementation of :navigate.\"\"\"\n \n@@ -219,10 +204,10 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,\n         if window:\n             new_window = mainwindow.MainWindow(\n                 private=cur_tabbed_browser.is_private)\n-            new_window.show()\n             tabbed_browser = objreg.get('tabbed-browser', scope='window',\n                                         window=new_window.win_id)\n             tabbed_browser.tabopen(url, background=False)\n+            new_window.show()\n         elif tab:\n             cur_tabbed_browser.tabopen(url, background=background)\n         else:\ndiff --git a/qutebrowser/browser/network/__init__.py b/qutebrowser/browser/network/__init__.py\nindex c3d713ac2..92c677c29 100644\n--- a/qutebrowser/browser/network/__init__.py\n+++ b/qutebrowser/browser/network/__init__.py\n@@ -1,3 +1 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n \"\"\"Modules related to network operations.\"\"\"\ndiff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py\nindex be25a2a41..656c620db 100644\n--- a/qutebrowser/browser/network/pac.py\n+++ b/qutebrowser/browser/network/pac.py\n@@ -1,35 +1,21 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Evaluation of PAC scripts.\"\"\"\n \n import sys\n import functools\n-from typing import Optional\n+from typing import Optional, cast\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import QObject, pyqtSignal, pyqtSlot, QUrl\n from qutebrowser.qt.network import (QNetworkProxy, QNetworkRequest, QHostInfo,\n                              QNetworkReply, QNetworkAccessManager,\n                              QHostAddress)\n from qutebrowser.qt.qml import QJSEngine, QJSValue\n \n-from qutebrowser.utils import log, utils, qtutils, resources\n+from qutebrowser.utils import log, qtlog, utils, qtutils, resources, urlutils\n \n \n class ParseProxyError(Exception):\n@@ -66,7 +52,8 @@ def _js_slot(*args):\n                 return self._error_con.callAsConstructor([e])\n                 # pylint: enable=protected-access\n \n-        deco = pyqtSlot(*args, result=QJSValue)\n+        # FIXME:mypy PyQt6 stubs issue, passing type should work too\n+        deco = pyqtSlot(*args, result=\"QJSValue\")\n         return deco(new_method)\n     return _decorator\n \n@@ -214,13 +201,20 @@ class PACResolver:\n         \"\"\"\n         qtutils.ensure_valid(query.url())\n \n+        string_flags: urlutils.UrlFlagsType\n         if from_file:\n             string_flags = QUrl.ComponentFormattingOption.PrettyDecoded\n         else:\n-            string_flags = QUrl.UrlFormattingOption.RemoveUserInfo  # type: ignore[assignment]\n+            string_flags = QUrl.UrlFormattingOption.RemoveUserInfo\n             if query.url().scheme() == 'https':\n-                string_flags |= QUrl.UrlFormattingOption.RemovePath  # type: ignore[assignment]\n-                string_flags |= QUrl.UrlFormattingOption.RemoveQuery  # type: ignore[assignment]\n+                https_opts = (\n+                    QUrl.UrlFormattingOption.RemovePath |\n+                    QUrl.UrlFormattingOption.RemoveQuery)\n+\n+                if machinery.IS_QT5:\n+                    string_flags |= cast(QUrl.UrlFormattingOption, https_opts)\n+                else:\n+                    string_flags |= https_opts\n \n         result = self._resolver.call([query.url().toString(string_flags),\n                                       query.peerHostName()])\n@@ -251,7 +245,7 @@ class PACFetcher(QObject):\n         url.setScheme(url.scheme()[len(pac_prefix):])\n \n         self._pac_url = url\n-        with log.disable_qt_msghandler():\n+        with qtlog.disable_qt_msghandler():\n             # WORKAROUND for a hang when messages are printed, see our\n             # NetworkAccessManager subclass for details.\n             self._manager: Optional[QNetworkAccessManager] = QNetworkAccessManager()\n@@ -270,6 +264,7 @@ class PACFetcher(QObject):\n         \"\"\"Fetch the proxy from the remote URL.\"\"\"\n         assert self._manager is not None\n         self._reply = self._manager.get(QNetworkRequest(self._pac_url))\n+        assert self._reply is not None\n         self._reply.finished.connect(self._finish)\n \n     @pyqtSlot()\n@@ -287,6 +282,8 @@ class PACFetcher(QObject):\n                 error = \"Invalid encoding of a PAC file: {}\"\n                 self._error_message = error.format(e)\n                 log.network.exception(self._error_message)\n+                return\n+\n             try:\n                 self._pac = PACResolver(pacscript)\n                 log.network.debug(\"Successfully evaluated PAC file.\")\n@@ -294,6 +291,7 @@ class PACFetcher(QObject):\n                 error = \"Error in PAC evaluation: {}\"\n                 self._error_message = error.format(e)\n                 log.network.exception(self._error_message)\n+\n         self._manager = None\n         self._reply = None\n         self.finished.emit()\ndiff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py\nindex 4022337c4..62872d68e 100644\n--- a/qutebrowser/browser/network/proxy.py\n+++ b/qutebrowser/browser/network/proxy.py\n@@ -1,34 +1,21 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Handling of proxies.\"\"\"\n \n+from typing import Optional\n+\n from qutebrowser.qt.core import QUrl, pyqtSlot\n from qutebrowser.qt.network import QNetworkProxy, QNetworkProxyFactory\n \n from qutebrowser.config import config, configtypes\n-from qutebrowser.utils import message, usertypes, urlutils, utils\n+from qutebrowser.utils import message, usertypes, urlutils, utils, qtutils\n from qutebrowser.misc import objects\n from qutebrowser.browser.network import pac\n \n \n-application_factory = None\n+application_factory: Optional[\"ProxyFactory\"] = None\n \n \n def init():\n@@ -53,7 +40,7 @@ def _warn_for_pac():\n @pyqtSlot()\n def shutdown():\n     QNetworkProxyFactory.setApplicationProxyFactory(\n-        None)  # type: ignore[arg-type]\n+        qtutils.QT_NONE)\n \n \n class ProxyFactory(QNetworkProxyFactory):\n@@ -97,7 +84,7 @@ class ProxyFactory(QNetworkProxyFactory):\n         if proxy is configtypes.SYSTEM_PROXY:\n             # On Linux, use \"export http_proxy=socks5://host:port\" to manually\n             # set system proxy.\n-            # ref. https://doc.qt.io/qt-5/qnetworkproxyfactory.html#systemProxyForQuery\n+            # ref. https://doc.qt.io/qt-6/qnetworkproxyfactory.html#systemProxyForQuery\n             proxies = QNetworkProxyFactory.systemProxyForQuery(query)\n         elif isinstance(proxy, pac.PACFetcher):\n             if objects.backend == usertypes.Backend.QtWebEngine:\ndiff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py\nindex 4b86b4c27..7062febb1 100644\n--- a/qutebrowser/browser/pdfjs.py\n+++ b/qutebrowser/browser/pdfjs.py\n@@ -1,22 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-# Copyright 2015 Daniel Schadt\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Daniel Schadt\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"pdf.js integration for qutebrowser.\"\"\"\n \n@@ -24,7 +9,7 @@ import os\n \n from qutebrowser.qt.core import QUrl, QUrlQuery\n \n-from qutebrowser.utils import resources, javascript, jinja, standarddir, log\n+from qutebrowser.utils import resources, javascript, jinja, standarddir, log, urlutils\n from qutebrowser.config import config\n \n \n@@ -76,14 +61,29 @@ def generate_pdfjs_page(filename, url):\n     html = html.replace('',\n                         '{}'.format(script))\n     # WORKAROUND for the fact that PDF.js tries to use the Fetch API even with\n-    # qute:// URLs.\n-    pdfjs_script = ''\n-    html = html.replace(pdfjs_script,\n-                        'window.Response = undefined;\\n' +\n-                        pdfjs_script)\n+    # qute:// URLs, this is probably no longer needed in PDFjs 4+. See #4235\n+    html = html.replace(\n+        '',\n+        '\\nwindow.Response = undefined;\\n'\n+    )\n     return html\n \n \n+def _generate_polyfills():\n+    return \"\"\"\n+        if (typeof Promise.withResolvers === 'undefined') {\n+            Promise.withResolvers = function () {\n+                let resolve, reject\n+                const promise = new Promise((res, rej) =&gt; {\n+                    resolve = res\n+                    reject = rej\n+                })\n+                return { promise, resolve, reject }\n+            }\n+        }\n+    \"\"\"\n+\n+\n def _generate_pdfjs_script(filename):\n     \"\"\"Generate the script that shows the pdf with pdf.js.\n \n@@ -95,24 +95,33 @@ def _generate_pdfjs_script(filename):\n     url_query.addQueryItem('filename', filename)\n     url.setQuery(url_query)\n \n-    js_url = javascript.to_js(\n-        url.toString(QUrl.ComponentFormattingOption.FullyEncoded))  # type: ignore[arg-type]\n+    js_url = javascript.to_js(url.toString(urlutils.FormatOption.ENCODED))\n \n     return jinja.js_environment.from_string(\"\"\"\n+        {{ polyfills }}\n+\n         document.addEventListener(\"DOMContentLoaded\", function() {\n-          if (typeof window.PDFJS !== 'undefined') {\n-              // v1.x\n-              window.PDFJS.verbosity = window.PDFJS.VERBOSITY_LEVELS.info;\n-          } else {\n-              // v2.x\n-              const options = window.PDFViewerApplicationOptions;\n-              options.set('verbosity', pdfjsLib.VerbosityLevel.INFOS);\n-          }\n-\n-          const viewer = window.PDFView || window.PDFViewerApplication;\n-          viewer.open({{ url }});\n+            if (typeof window.PDFJS !== 'undefined') {\n+                // v1.x\n+                window.PDFJS.verbosity = window.PDFJS.VERBOSITY_LEVELS.info;\n+            } else {\n+                // v2.x+\n+                const options = window.PDFViewerApplicationOptions;\n+                options.set('verbosity', pdfjsLib.VerbosityLevel.INFOS);\n+            }\n+\n+            if (typeof window.PDFView !== 'undefined') {\n+                // &lt; v1.6\n+                window.PDFView.open({{ url }});\n+            } else {\n+                // v1.6+\n+                window.PDFViewerApplication.open({\n+                    url: {{ url }},\n+                    originalUrl: {{ url }}\n+                });\n+            }\n         });\n-    \"\"\").render(url=js_url)\n+    \"\"\").render(url=js_url, polyfills=_generate_polyfills())\n \n \n def get_pdfjs_res_and_path(path):\n@@ -156,6 +165,14 @@ def get_pdfjs_res_and_path(path):\n             log.misc.warning(\"OSError while reading PDF.js file: {}\".format(e))\n             raise PDFJSNotFound(path) from None\n \n+    if path == \"build/pdf.worker.mjs\":\n+        content = b\"\\n\".join(\n+            [\n+                _generate_polyfills().encode(\"ascii\"),\n+                content,\n+            ]\n+        )\n+\n     return content, file_path\n \n \n@@ -210,10 +227,24 @@ def _read_from_system(system_path, names):\n     return (None, None)\n \n \n+def get_pdfjs_js_path():\n+    \"\"\"Checks for pdf.js main module availability and returns the path if available.\"\"\"\n+    paths = ['build/pdf.js', 'build/pdf.mjs']\n+    for path in paths:\n+        try:\n+            get_pdfjs_res(path)\n+        except PDFJSNotFound:\n+            pass\n+        else:\n+            return path\n+\n+    raise PDFJSNotFound(\" or \".join(paths))\n+\n+\n def is_available():\n-    \"\"\"Return true if a pdfjs installation is available.\"\"\"\n+    \"\"\"Return true if certain parts of a pdfjs installation are available.\"\"\"\n     try:\n-        get_pdfjs_res('build/pdf.js')\n+        get_pdfjs_js_path()\n         get_pdfjs_res('web/viewer.html')\n     except PDFJSNotFound:\n         return False\n@@ -237,7 +268,7 @@ def get_main_url(filename: str, original_url: QUrl) -&gt; QUrl:\n     query = QUrlQuery()\n     query.addQueryItem('filename', filename)  # read from our JS\n     query.addQueryItem('file', '')  # to avoid pdfjs opening the default PDF\n-    urlstr = original_url.toString(QUrl.ComponentFormattingOption.FullyEncoded)  # type: ignore[arg-type]\n+    urlstr = original_url.toString(urlutils.FormatOption.ENCODED)\n     query.addQueryItem('source', urlstr)\n     url.setQuery(query)\n     return url\ndiff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py\nindex 46d51d930..63122208f 100644\n--- a/qutebrowser/browser/qtnetworkdownloads.py\n+++ b/qutebrowser/browser/qtnetworkdownloads.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Download manager.\"\"\"\n \n@@ -31,10 +16,10 @@ from qutebrowser.qt.widgets import QApplication\n from qutebrowser.qt.network import QNetworkRequest, QNetworkReply, QNetworkAccessManager\n \n from qutebrowser.config import config, websettings\n-from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg\n+from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg, qtlog\n from qutebrowser.misc import quitter\n from qutebrowser.browser import downloads\n-from qutebrowser.browser.webkit import http\n+from qutebrowser.browser.webkit import httpheaders\n from qutebrowser.browser.webkit.network import networkmanager\n \n \n@@ -62,12 +47,8 @@ class DownloadItem(downloads.AbstractDownloadItem):\n     As soon as we know the file object, we copy self._buffer over and the next\n     readyRead will write to the real file object.\n \n-    Class attributes:\n-        _MAX_REDIRECTS: The maximum redirection count.\n-\n     Attributes:\n         _retry_info: A _RetryInfo instance.\n-        _redirects: How many time we were redirected already.\n         _buffer: A BytesIO object to buffer incoming data until we know the\n                  target file.\n         _read_timer: A Timer which reads the QNetworkReply into self._buffer\n@@ -82,7 +63,6 @@ class DownloadItem(downloads.AbstractDownloadItem):\n                         arg 0: The new DownloadItem\n     \"\"\"\n \n-    _MAX_REDIRECTS = 10\n     adopt_download = pyqtSignal(object)  # DownloadItem\n \n     def __init__(self, reply, manager):\n@@ -102,7 +82,6 @@ class DownloadItem(downloads.AbstractDownloadItem):\n         self._read_timer = usertypes.Timer(self, name='download-read-timer')\n         self._read_timer.setInterval(500)\n         self._read_timer.timeout.connect(self._on_read_timer_timeout)\n-        self._redirects = 0\n         self._url = reply.url()\n         self._init_reply(reply)\n \n@@ -123,11 +102,13 @@ class DownloadItem(downloads.AbstractDownloadItem):\n         if self._reply is None:\n             log.downloads.debug(\"Reply gone while dying\")\n             return\n+\n         self._reply.downloadProgress.disconnect()\n         self._reply.finished.disconnect()\n-        self._reply.error.disconnect()\n+        self._reply.errorOccurred.disconnect()\n         self._reply.readyRead.disconnect()\n-        with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal '\n+\n+        with qtlog.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal '\n                                  'problem, this method must only be called '\n                                  'once.'):\n             # See https://codereview.qt-project.org/#/c/107863/\n@@ -135,11 +116,22 @@ class DownloadItem(downloads.AbstractDownloadItem):\n         self._reply.deleteLater()\n         self._reply = None\n         if self.fileobj is not None:\n+            pos = self.fileobj.tell()\n+            log.downloads.debug(f\"File position at error: {pos}\")\n             try:\n                 self.fileobj.close()\n             except OSError:\n                 log.downloads.exception(\"Error while closing file object\")\n \n+            if pos == 0:\n+                # Empty remaining file\n+                filename = self._get_open_filename()\n+                log.downloads.debug(f\"Removing empty file at {filename}\")\n+                try:\n+                    os.remove(filename)\n+                except OSError:\n+                    log.downloads.exception(\"Error while removing empty file\")\n+\n     def _init_reply(self, reply):\n         \"\"\"Set a new reply and connect its signals.\n \n@@ -150,11 +142,14 @@ class DownloadItem(downloads.AbstractDownloadItem):\n         self.successful = False\n         self._reply = reply\n         reply.setReadBufferSize(16 * 1024 * 1024)  # 16 MB\n+\n         reply.downloadProgress.connect(self.stats.on_download_progress)\n         reply.finished.connect(self._on_reply_finished)\n-        reply.error.connect(self._on_reply_error)\n+        reply.errorOccurred.connect(self._on_reply_error)\n         reply.readyRead.connect(self._on_ready_read)\n         reply.metaDataChanged.connect(self._on_meta_data_changed)\n+        reply.redirected.connect(self._on_redirected)\n+\n         self._retry_info = _RetryInfo(request=reply.request(),\n                                       manager=reply.manager())\n         if not self.fileobj:\n@@ -165,6 +160,13 @@ class DownloadItem(downloads.AbstractDownloadItem):\n         if reply.error() != QNetworkReply.NetworkError.NoError:\n             QTimer.singleShot(0, lambda: self._die(reply.errorString()))\n \n+    @pyqtSlot(QUrl)\n+    def _on_redirected(self, url):\n+        if self._reply is None:\n+            log.downloads.warning(f\"redirected: REPLY GONE -&gt; {url}\")\n+        else:\n+            log.downloads.debug(f\"redirected: {self._reply.url()} -&gt; {url}\")\n+\n     def _do_cancel(self):\n         self._read_timer.stop()\n         if self._reply is not None:\n@@ -307,9 +309,6 @@ class DownloadItem(downloads.AbstractDownloadItem):\n             return\n         self._read_timer.stop()\n         self.stats.finish()\n-        is_redirected = self._handle_redirect()\n-        if is_redirected:\n-            return\n         log.downloads.debug(\"Reply finished, fileobj {}\".format(self.fileobj))\n         if self.fileobj is not None:\n             # We can do a \"delayed\" write immediately to empty the buffer and\n@@ -364,48 +363,6 @@ class DownloadItem(downloads.AbstractDownloadItem):\n         for key, value in self._reply.rawHeaderPairs():\n             self.raw_headers[bytes(key)] = bytes(value)\n \n-    def _handle_redirect(self):\n-        \"\"\"Handle an HTTP redirect.\n-\n-        Return:\n-            True if the download was redirected, False otherwise.\n-        \"\"\"\n-        assert self._reply is not None\n-        redirect = self._reply.attribute(\n-            QNetworkRequest.Attribute.RedirectionTargetAttribute)\n-        if redirect is None or redirect.isEmpty():\n-            return False\n-        new_url = self._reply.url().resolved(redirect)\n-        new_request = self._reply.request()\n-        if new_url == new_request.url():\n-            return False\n-\n-        if self._redirects &gt; self._MAX_REDIRECTS:\n-            self._die(\"Maximum redirection count reached!\")\n-            self.delete()\n-            return True  # so on_reply_finished aborts\n-\n-        log.downloads.debug(\"{}: Handling redirect\".format(self))\n-        self._redirects += 1\n-        new_request.setUrl(new_url)\n-\n-        old_reply = self._reply\n-        assert old_reply is not None\n-        old_reply.finished.disconnect(self._on_reply_finished)\n-\n-        self._read_timer.stop()\n-        self._reply = None\n-        if self.fileobj is not None:\n-            self.fileobj.seek(0)\n-\n-        log.downloads.debug(\"redirected: {} -&gt; {}\".format(\n-            old_reply.url(), new_request.url()))\n-        new_reply = old_reply.manager().get(new_request)\n-        self._init_reply(new_reply)\n-\n-        old_reply.deleteLater()\n-        return True\n-\n     def _uses_nam(self, nam):\n         \"\"\"Check if this download uses the given QNetworkAccessManager.\"\"\"\n         assert self._retry_info is not None\n@@ -422,8 +379,17 @@ class DownloadManager(downloads.AbstractDownloadManager):\n \n     Attributes:\n         _networkmanager: A NetworkManager for generic downloads.\n+\n+    Class attributes:\n+        _MAX_REDIRECTS: The maximum redirection count.\n     \"\"\"\n \n+    # Same as many browsers\n+    # https://fetch.spec.whatwg.org/#http-redirect-fetch\n+    # https://source.chromium.org/chromium/chromium/src/+/main:net/url_request/url_request.h;l=97;drc=3c19a2edb96d3d5b56a7481349a357fdbdf8ecf0\n+    # https://stackoverflow.com/questions/9384474/in-chrome-how-many-redirects-are-too-many\n+    _MAX_REDIRECTS = 20\n+\n     def __init__(self, parent=None):\n         super().__init__(parent)\n         self._networkmanager = networkmanager.NetworkManager(\n@@ -447,12 +413,20 @@ class DownloadManager(downloads.AbstractDownloadManager):\n             return None\n \n         req = QNetworkRequest(url)\n+\n         user_agent = websettings.user_agent(url)\n         req.setHeader(QNetworkRequest.KnownHeaders.UserAgentHeader, user_agent)\n-\n         if not cache:\n             req.setAttribute(QNetworkRequest.Attribute.CacheSaveControlAttribute, False)\n \n+        # Needed for Qt 5, default on Qt 6\n+        # We don't set this on the QNAM because QtWebKit handles redirects manually.\n+        req.setAttribute(\n+            QNetworkRequest.Attribute.RedirectPolicyAttribute,\n+            QNetworkRequest.RedirectPolicy.NoLessSafeRedirectPolicy,\n+        )\n+        req.setMaximumRedirectsAllowed(self._MAX_REDIRECTS)\n+\n         return self.get_request(req, **kwargs)\n \n     def get_mhtml(self, tab, target):\n@@ -559,7 +533,7 @@ class DownloadManager(downloads.AbstractDownloadManager):\n             try:\n                 suggested_filename = target.suggested_filename()\n             except downloads.NoFilenameError:\n-                _, suggested_filename = http.parse_content_disposition(reply)\n+                _, suggested_filename = httpheaders.parse_content_disposition(reply)\n         log.downloads.debug(\"fetch: {} -&gt; {}\".format(reply.url(),\n                                                      suggested_filename))\n         download = DownloadItem(reply, manager=self)\ndiff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py\nindex aecb9a11a..508d510d7 100644\n--- a/qutebrowser/browser/qutescheme.py\n+++ b/qutebrowser/browser/qutescheme.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Backend-independent qute://* code.\n \n@@ -24,6 +9,7 @@ Module attributes:\n     _HANDLERS: The handlers registered via decorators.\n \"\"\"\n \n+import sys\n import html\n import json\n import os\n@@ -46,10 +32,10 @@ from qutebrowser.qt import sip\n \n \n pyeval_output = \":pyeval was never called\"\n-csrf_token = None\n+csrf_token: Optional[str] = None\n \n \n-_HANDLERS = {}\n+_HANDLERS: Dict[str, \"_HandlerCallable\"] = {}\n \n \n class Error(Exception):\n@@ -500,10 +486,11 @@ def qute_back(url: QUrl) -&gt; _HandlerRet:\n \n \n @add_handler('configdiff')\n-def qute_configdiff(_url: QUrl) -&gt; _HandlerRet:\n+def qute_configdiff(url: QUrl) -&gt; _HandlerRet:\n     \"\"\"Handler for qute://configdiff.\"\"\"\n-    data = config.instance.dump_userconfig().encode('utf-8')\n-    return 'text/plain', data\n+    include_hidden = QUrlQuery(url).queryItemValue('include_hidden') == 'true'\n+    dump = config.instance.dump_userconfig(include_hidden=include_hidden)\n+    return 'text/plain', dump.encode('utf-8')\n \n \n @add_handler('pastebin-version')\n@@ -565,9 +552,8 @@ def qute_pdfjs(url: QUrl) -&gt; _HandlerRet:\n         log.misc.warning(\n             \"pdfjs resource requested but not found: {}\".format(e.path))\n         raise NotFoundError(\"Can't find pdfjs resource '{}'\".format(e.path))\n-    else:\n-        mimetype = utils.guess_mimetype(url.fileName(), fallback=True)\n-        return mimetype, data\n+    mimetype = utils.guess_mimetype(url.fileName(), fallback=True)\n+    return mimetype, data\n \n \n @add_handler('warning')\n@@ -582,6 +568,12 @@ def qute_warning(url: QUrl) -&gt; _HandlerRet:\n                            title='Qt 5.15 sessions warning',\n                            datadir=standarddir.data(),\n                            sep=os.sep)\n+    elif path == '/qt5':\n+        is_venv = hasattr(sys, 'real_prefix') or sys.base_prefix != sys.prefix\n+        src = jinja.render('warning-qt5.html',\n+                           title='Switch to Qt 6',\n+                           is_venv=is_venv,\n+                           prefix=sys.prefix)\n     else:\n         raise NotFoundError(\"Invalid warning page {}\".format(path))\n     return 'text/html', src\ndiff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py\nindex 0f9e0bd8d..358af6d95 100644\n--- a/qutebrowser/browser/shared.py\n+++ b/qutebrowser/browser/shared.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Various utilities shared between webpage/webview subclasses.\"\"\"\n \n@@ -31,7 +16,7 @@ from qutebrowser.qt.core import QUrl, pyqtBoundSignal\n \n from qutebrowser.config import config\n from qutebrowser.utils import (usertypes, message, log, objreg, jinja, utils,\n-                               qtutils, version)\n+                               qtutils, version, urlutils)\n from qutebrowser.mainwindow import mainwindow\n from qutebrowser.misc import guiprocess, objects\n \n@@ -205,13 +190,13 @@ def javascript_log_message(\n     logger(logstring)\n \n \n-def ignore_certificate_error(\n+def handle_certificate_error(\n         *,\n         request_url: QUrl,\n         first_party_url: QUrl,\n         error: usertypes.AbstractCertificateErrorWrapper,\n         abort_on: Iterable[pyqtBoundSignal],\n-) -&gt; bool:\n+) -&gt; None:\n     \"\"\"Display a certificate error question.\n \n     Args:\n@@ -219,9 +204,6 @@ def ignore_certificate_error(\n         first_party_url: The URL of the page we're visiting. Might be an invalid QUrl.\n         error: A single error.\n         abort_on: Signals aborting a question.\n-\n-    Return:\n-        True if the error should be ignored, False otherwise.\n     \"\"\"\n     conf = config.instance.get('content.tls.certificate_errors', url=request_url)\n     log.network.debug(f\"Certificate error {error!r}, config {conf}\")\n@@ -232,9 +214,7 @@ def ignore_certificate_error(\n     # scheme might not match.\n     is_resource = (\n         first_party_url.isValid() and\n-        not request_url.matches(\n-            first_party_url,\n-            QUrl.UrlFormattingOption.RemoveScheme))  # type: ignore[arg-type]\n+        not request_url.matches(first_party_url, urlutils.FormatOption.REMOVE_SCHEME))\n \n     if conf == 'ask' or conf == 'ask-block-thirdparty' and not is_resource:\n         err_template = jinja.environment.from_string(\"\"\"\n@@ -263,28 +243,46 @@ def ignore_certificate_error(\n             is_resource=is_resource,\n             error=error,\n         )\n-\n         urlstr = request_url.toString(\n-            QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)  # type: ignore[arg-type]\n-        ignore = message.ask(title=\"Certificate error\", text=msg,\n-                             mode=usertypes.PromptMode.yesno, default=False,\n-                             abort_on=abort_on, url=urlstr)\n-        if ignore is None:\n-            # prompt aborted\n-            ignore = False\n-        return ignore\n+            urlutils.FormatOption.REMOVE_PASSWORD | urlutils.FormatOption.ENCODED)\n+        title = \"Certificate error\"\n+\n+        try:\n+            error.defer()\n+        except usertypes.UndeferrableError:\n+            # QtNetwork / QtWebKit and buggy PyQt versions\n+            # Show blocking question prompt\n+            ignore = message.ask(title=title, text=msg,\n+                                 mode=usertypes.PromptMode.yesno, default=False,\n+                                 abort_on=abort_on, url=urlstr)\n+            if ignore:\n+                error.accept_certificate()\n+            else:  # includes None, i.e. prompt aborted\n+                error.reject_certificate()\n+        else:\n+            # Show non-blocking question prompt\n+            message.confirm_async(\n+                title=title,\n+                text=msg,\n+                abort_on=abort_on,\n+                url=urlstr,\n+                yes_action=error.accept_certificate,\n+                no_action=error.reject_certificate,\n+                cancel_action=error.reject_certificate,\n+            )\n     elif conf == 'load-insecurely':\n         message.error(f'Certificate error: {error}')\n-        return True\n+        error.accept_certificate()\n     elif conf == 'block':\n-        return False\n+        error.reject_certificate()\n     elif conf == 'ask-block-thirdparty' and is_resource:\n         log.network.error(\n             f\"Certificate error in resource load: {error}\\n\"\n             f\"  request URL:     {request_url.toDisplayString()}\\n\"\n             f\"  first party URL: {first_party_url.toDisplayString()}\")\n-        return False\n-    raise utils.Unreachable(conf, is_resource)\n+        error.reject_certificate()\n+    else:\n+        raise utils.Unreachable(conf, is_resource)\n \n \n def feature_permission(url, option, msg, yes_action, no_action, abort_on,\n@@ -345,23 +343,19 @@ def get_tab(win_id, target):\n         win_id: The window ID to open new tabs in\n         target: A usertypes.ClickTarget\n     \"\"\"\n-    if target == usertypes.ClickTarget.tab:\n-        bg_tab = False\n-    elif target == usertypes.ClickTarget.tab_bg:\n-        bg_tab = True\n-    elif target == usertypes.ClickTarget.window:\n-        tabbed_browser = objreg.get('tabbed-browser', scope='window',\n-                                    window=win_id)\n+    tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id)\n+    if target == usertypes.ClickTarget.window:\n         window = mainwindow.MainWindow(private=tabbed_browser.is_private)\n+        tab = window.tabbed_browser.tabopen(url=None, background=False)\n         window.show()\n-        win_id = window.win_id\n-        bg_tab = False\n-    else:\n-        raise ValueError(\"Invalid ClickTarget {}\".format(target))\n+        return tab\n+    elif target in [usertypes.ClickTarget.tab, usertypes.ClickTarget.tab_bg]:\n+        return tabbed_browser.tabopen(\n+            url=None,\n+            background=target == usertypes.ClickTarget.tab_bg,\n+        )\n \n-    tabbed_browser = objreg.get('tabbed-browser', scope='window',\n-                                window=win_id)\n-    return tabbed_browser.tabopen(url=None, background=bg_tab)\n+    raise ValueError(f\"Invalid ClickTarget {target}\")\n \n \n def get_user_stylesheet(searching=False):\n@@ -381,7 +375,7 @@ def get_user_stylesheet(searching=False):\n         css += '\\nhtml &gt; ::-webkit-scrollbar { width: 0px; height: 0px; }'\n \n     if (objects.backend == usertypes.Backend.QtWebEngine and\n-            version.qtwebengine_versions().chromium_major in [69, 73, 80, 87] and\n+            version.qtwebengine_versions().chromium_major in [87, 90] and\n             config.val.colors.webpage.darkmode.enabled and\n             config.val.colors.webpage.darkmode.policy.images == 'smart' and\n             config.val.content.site_specific_quirks.enabled and\ndiff --git a/qutebrowser/browser/signalfilter.py b/qutebrowser/browser/signalfilter.py\nindex beb91e70a..3ca0f89db 100644\n--- a/qutebrowser/browser/signalfilter.py\n+++ b/qutebrowser/browser/signalfilter.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"A filter for signals which either filters or passes them.\"\"\"\n \ndiff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py\nindex 0d30f7973..2d2563a1a 100644\n--- a/qutebrowser/browser/urlmarks.py\n+++ b/qutebrowser/browser/urlmarks.py\n@@ -1,22 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-# Copyright 2015-2018 Antoni Boucher \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Antoni Boucher \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Managers for bookmarks and quickmarks.\n \n@@ -113,6 +98,11 @@ class UrlMarkManager(QObject):\n         del self.marks[key]\n         self.changed.emit()\n \n+    def clear(self):\n+        \"\"\"Delete all marks.\"\"\"\n+        self.marks.clear()\n+        self.changed.emit()\n+\n \n class QuickmarkManager(UrlMarkManager):\n \ndiff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py\nindex 805e88542..721ab83df 100644\n--- a/qutebrowser/browser/webelem.py\n+++ b/qutebrowser/browser/webelem.py\n@@ -1,28 +1,14 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Generic web element related code.\"\"\"\n \n-from typing import cast, TYPE_CHECKING, Iterator, Optional, Set, Union, Dict\n+from typing import Iterator, Optional, Set, TYPE_CHECKING, Union, Dict\n import collections.abc\n \n-from qutebrowser.qt.core import QUrl, Qt, QEvent, QTimer, QRect, QPoint\n+from qutebrowser.qt import machinery\n+from qutebrowser.qt.core import QUrl, Qt, QEvent, QTimer, QRect, QPointF\n from qutebrowser.qt.gui import QMouseEvent\n \n from qutebrowser.config import config\n@@ -35,6 +21,11 @@ if TYPE_CHECKING:\n \n JsValueType = Union[int, float, str, None]\n \n+if machinery.IS_QT6:\n+    KeybordModifierType = Qt.KeyboardModifier\n+else:\n+    KeybordModifierType = Union[Qt.KeyboardModifiers, Qt.KeyboardModifier]\n+\n \n class Error(Exception):\n \n@@ -317,7 +308,7 @@ class AbstractWebElement(collections.abc.MutableMapping):  # type: ignore[type-a\n         \"\"\"Return True if clicking this element needs user interaction.\"\"\"\n         raise NotImplementedError\n \n-    def _mouse_pos(self) -&gt; QPoint:\n+    def _mouse_pos(self) -&gt; QPointF:\n         \"\"\"Get the position to click/hover.\"\"\"\n         # Click the center of the largest square fitting into the top/left\n         # corner of the rectangle, this will help if part of the  element\n@@ -331,7 +322,7 @@ class AbstractWebElement(collections.abc.MutableMapping):  # type: ignore[type-a\n         pos = rect.center()\n         if pos.x() &lt; 0 or pos.y() &lt; 0:\n             raise Error(\"Element position is out of view!\")\n-        return pos\n+        return QPointF(pos)\n \n     def _move_text_cursor(self) -&gt; None:\n         \"\"\"Move cursor to end after clicking.\"\"\"\n@@ -345,7 +336,7 @@ class AbstractWebElement(collections.abc.MutableMapping):  # type: ignore[type-a\n         log.webelem.debug(\"Sending fake click to {!r} at position {} with \"\n                           \"target {}\".format(self, pos, click_target))\n \n-        target_modifiers: Dict[usertypes.ClickTarget, Qt.KeyboardModifier] = {\n+        target_modifiers: Dict[usertypes.ClickTarget, KeybordModifierType] = {\n             usertypes.ClickTarget.normal: Qt.KeyboardModifier.NoModifier,\n             usertypes.ClickTarget.window: Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.ShiftModifier,\n             usertypes.ClickTarget.tab: Qt.KeyboardModifier.ControlModifier,\n@@ -364,10 +355,14 @@ class AbstractWebElement(collections.abc.MutableMapping):  # type: ignore[type-a\n             QMouseEvent(QEvent.Type.MouseButtonRelease, pos, button, Qt.MouseButton.NoButton, modifiers),\n         ]\n \n-        for evt in events:\n-            self._tab.send_event(evt)\n+        def _send_events_after_delay() -&gt; None:\n+            \"\"\"Delay clicks to workaround timing issue in e2e tests on 6.7.\"\"\"\n+            for evt in events:\n+                self._tab.send_event(evt)\n+\n+            QTimer.singleShot(0, self._move_text_cursor)\n \n-        QTimer.singleShot(0, self._move_text_cursor)\n+        QTimer.singleShot(10, _send_events_after_delay)\n \n     def _click_editable(self, click_target: usertypes.ClickTarget) -&gt; None:\n         \"\"\"Fake a click on an editable input field.\"\"\"\n@@ -399,8 +394,8 @@ class AbstractWebElement(collections.abc.MutableMapping):  # type: ignore[type-a\n         elif click_target == usertypes.ClickTarget.window:\n             from qutebrowser.mainwindow import mainwindow\n             window = mainwindow.MainWindow(private=tabbed_browser.is_private)\n-            window.show()\n             window.tabbed_browser.tabopen(url)\n+            window.show()\n         else:\n             raise ValueError(\"Unknown ClickTarget {}\".format(click_target))\n \ndiff --git a/qutebrowser/browser/webengine/__init__.py b/qutebrowser/browser/webengine/__init__.py\nindex e86e96d39..913596a85 100644\n--- a/qutebrowser/browser/webengine/__init__.py\n+++ b/qutebrowser/browser/webengine/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Classes related to the browser widgets for QtWebEngine.\"\"\"\ndiff --git a/qutebrowser/browser/webengine/certificateerror.py b/qutebrowser/browser/webengine/certificateerror.py\nindex 19007a499..3403941fa 100644\n--- a/qutebrowser/browser/webengine/certificateerror.py\n+++ b/qutebrowser/browser/webengine/certificateerror.py\n@@ -1,24 +1,12 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Wrapper over a QWebEngineCertificateError.\"\"\"\n \n+from typing import Any\n+\n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import QUrl\n from qutebrowser.qt.webenginecore import QWebEngineCertificateError\n \n@@ -27,19 +15,43 @@ from qutebrowser.utils import usertypes, utils, debug\n \n class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):\n \n-    \"\"\"A wrapper over a QWebEngineCertificateError.\"\"\"\n+    \"\"\"A wrapper over a QWebEngineCertificateError.\n+\n+    Support both Qt 5 and 6.\n+    \"\"\"\n \n     def __init__(self, error: QWebEngineCertificateError) -&gt; None:\n+        super().__init__()\n         self._error = error\n         self.ignore = False\n \n     def __str__(self) -&gt; str:\n-        return self._error.errorDescription()\n+        if machinery.IS_QT5:\n+            return self._error.errorDescription()\n+        else:\n+            return self._error.description()\n+\n+    def _type(self) -&gt; Any:  # QWebEngineCertificateError.Type or .Error\n+        if machinery.IS_QT5:\n+            return self._error.error()\n+        else:\n+            return self._error.type()\n+\n+    def reject_certificate(self) -&gt; None:\n+        super().reject_certificate()\n+        self._error.rejectCertificate()\n+\n+    def accept_certificate(self) -&gt; None:\n+        super().accept_certificate()\n+        if machinery.IS_QT5:\n+            self._error.ignoreCertificateError()\n+        else:\n+            self._error.acceptCertificate()\n \n     def __repr__(self) -&gt; str:\n         return utils.get_repr(\n             self,\n-            error=debug.qenum_key(QWebEngineCertificateError, self._error.error()),\n+            error=debug.qenum_key(QWebEngineCertificateError, self._type()),\n             string=str(self))\n \n     def url(self) -&gt; QUrl:\n@@ -47,3 +59,8 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):\n \n     def is_overridable(self) -&gt; bool:\n         return self._error.isOverridable()\n+\n+    def defer(self) -&gt; None:\n+        # WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044585.html\n+        # (PyQt 5.15.6, 6.2.3, 6.3.0)\n+        raise usertypes.UndeferrableError(\"PyQt bug\")\ndiff --git a/qutebrowser/browser/webengine/cookies.py b/qutebrowser/browser/webengine/cookies.py\nindex 3a5a621cc..9d0e0f33a 100644\n--- a/qutebrowser/browser/webengine/cookies.py\n+++ b/qutebrowser/browser/webengine/cookies.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Filter for QtWebEngine cookies.\"\"\"\n \ndiff --git a/qutebrowser/browser/webengine/darkmode.py b/qutebrowser/browser/webengine/darkmode.py\nindex 1c6530b49..8f1908547 100644\n--- a/qutebrowser/browser/webengine/darkmode.py\n+++ b/qutebrowser/browser/webengine/darkmode.py\n@@ -1,28 +1,13 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Get darkmode arguments to pass to Qt.\n \n Overview of blink setting names based on the Qt version:\n \n-Qt 5.10\n--------\n+Qt 5.10 (UNSUPPORTED)\n+---------------------\n \n First implementation, called \"high contrast mode\".\n \n@@ -31,16 +16,16 @@ First implementation, called \"high contrast mode\".\n - highContrastContrast (float)\n - highContractImagePolicy (kFilterAll/kFilterNone)\n \n-Qt 5.11, 5.12, 5.13\n--------------------\n+Qt 5.11, 5.12, 5.13 (UNSUPPORTED)\n+---------------------------------\n \n New \"smart\" image policy.\n \n - Mode/Grayscale/Contrast as above\n - highContractImagePolicy (kFilterAll/kFilterNone/kFilterSmart [new!])\n \n-Qt 5.14\n--------\n+Qt 5.14 (UNSUPPORTED)\n+---------------------\n \n Renamed to \"darkMode\".\n \n@@ -54,8 +39,8 @@ Renamed to \"darkMode\".\n - darkModeBackgroundBrightnessThreshold (int) [new!]\n - darkModeImageGrayscale (float) [new!]\n \n-Qt 5.15.0 and 5.15.1\n---------------------\n+Qt 5.15.0 and 5.15.1 (UNSUPPORTED)\n+----------------------------------\n \n \"darkMode\" split into \"darkModeEnabled\" and \"darkModeInversionAlgorithm\".\n \n@@ -90,6 +75,49 @@ https://chromium-review.googlesource.com/c/chromium/src/+/2232922\n \n - Now needs to be 0 for dark and 1 for light\n   (before: 0 no preference / 1 dark / 2 light)\n+\n+Qt 6.2\n+------\n+\n+No significant changes over 5.15.3\n+\n+Qt 6.3\n+------\n+\n+- New IncreaseTextContrast:\n+  https://chromium-review.googlesource.com/c/chromium/src/+/2893236\n+  (UNSUPPORTED because dropped in 6.5)\n+\n+Qt 6.4\n+------\n+\n+- Renamed TextBrightnessThreshold to ForegroundBrightnessThreshold\n+\n+  \"Correct brightness threshold of darkmode color classifier\"\n+  https://chromium-review.googlesource.com/c/chromium/src/+/3344100\n+\n+  \"Rename text_classifier to foreground_classifier\"\n+  https://chromium-review.googlesource.com/c/chromium/src/+/3226389\n+\n+- Grayscale darkmode support removed:\n+  https://chromium-review.googlesource.com/c/chromium/src/+/3238985\n+\n+Qt 6.5\n+------\n+\n+- IncreaseTextContrast removed:\n+  https://chromium-review.googlesource.com/c/chromium/src/+/3821841\n+\n+Qt 6.6\n+------\n+\n+- New alternative image classifier:\n+  https://chromium-review.googlesource.com/c/chromium/src/+/3987823\n+\n+Qt 6.7\n+------\n+\n+Enabling dark mode can now be done at runtime via QWebEngineSettings.\n \"\"\"\n \n import os\n@@ -103,6 +131,10 @@ from typing import (Any, Iterator, Mapping, MutableMapping, Optional, Set, Tuple\n from qutebrowser.config import config\n from qutebrowser.utils import usertypes, utils, log, version\n \n+# Note: We *cannot* initialize QtWebEngine (even implicitly) in here, but checking for\n+# the enum attribute seems to be okay.\n+from qutebrowser.qt.webenginecore import QWebEngineSettings\n+\n \n _BLINK_SETTINGS = 'blink-settings'\n \n@@ -111,12 +143,11 @@ class Variant(enum.Enum):\n \n     \"\"\"A dark mode variant.\"\"\"\n \n-    qt_511_to_513 = enum.auto()\n-    qt_514 = enum.auto()\n-    qt_515_0 = enum.auto()\n-    qt_515_1 = enum.auto()\n     qt_515_2 = enum.auto()\n     qt_515_3 = enum.auto()\n+    qt_64 = enum.auto()\n+    qt_66 = enum.auto()\n+    qt_67 = enum.auto()\n \n \n # Mapping from a colors.webpage.darkmode.algorithm setting value to\n@@ -128,9 +159,6 @@ _ALGORITHMS = {\n     'lightness-hsl': 3,  # kInvertLightness\n     'lightness-cielab': 4,  # kInvertLightnessLAB\n }\n-# kInvertLightnessLAB is not available with Qt &lt; 5.14\n-_ALGORITHMS_BEFORE_QT_514 = _ALGORITHMS.copy()\n-_ALGORITHMS_BEFORE_QT_514['lightness-cielab'] = _ALGORITHMS['lightness-hsl']\n # Qt &gt;= 5.15.3, based on dark_mode_settings.h\n _ALGORITHMS_NEW = {\n     # 0: kSimpleInvertForTesting (not exposed)\n@@ -146,6 +174,15 @@ _IMAGE_POLICIES = {\n     'always': 0,  # kFilterAll\n     'never': 1,  # kFilterNone\n     'smart': 2,  # kFilterSmart\n+    'smart-simple': 2,  # kFilterSmart\n+}\n+\n+# Using the colors.webpage.darkmode.policy.images setting, shared with _IMAGE_POLICIES\n+_IMAGE_CLASSIFIERS = {\n+    'always': None,\n+    'never': None,\n+    'smart': 0,  # kNumColorsWithMlFallback\n+    'smart-simple': 1,  # kTransparencyAndNumColors\n }\n \n # Mapping from a colors.webpage.darkmode.policy.page setting value to\n@@ -168,14 +205,17 @@ class _Setting:\n \n     option: str\n     chromium_key: str\n-    mapping: Optional[Mapping[Any, Union[str, int]]] = None\n+    mapping: Optional[Mapping[Any, Union[str, int, None]]] = None\n \n     def _value_str(self, value: Any) -&gt; str:\n         if self.mapping is None:\n             return str(value)\n         return str(self.mapping[value])\n \n-    def chromium_tuple(self, value: Any) -&gt; Tuple[str, str]:\n+    def chromium_tuple(self, value: Any) -&gt; Optional[Tuple[str, str]]:\n+        \"\"\"Get the Chromium key and value, or None if no value should be set.\"\"\"\n+        if self.mapping is not None and self.mapping[value] is None:\n+            return None\n         return self.chromium_key, self._value_str(value)\n \n     def with_prefix(self, prefix: str) -&gt; '_Setting':\n@@ -225,111 +265,103 @@ class _Definition:\n             switch = self._switch_names.get(setting.option, self._switch_names[None])\n             yield switch, setting.with_prefix(self.prefix)\n \n-    def copy_with(self, attr: str, value: Any) -&gt; '_Definition':\n-        \"\"\"Get a new _Definition object with a changed attribute.\n-\n-        NOTE: This does *not* copy the settings list. Both objects will reference the\n-        same list.\n-        \"\"\"\n+    def copy_add_setting(self, setting: _Setting) -&gt; '_Definition':\n+        \"\"\"Get a new _Definition object with an additional setting.\"\"\"\n         new = copy.copy(self)\n-        setattr(new, attr, value)\n+        new._settings = self._settings + (setting,)  # pylint: disable=protected-access\n         return new\n \n+    def copy_remove_setting(self, name: str) -&gt; '_Definition':\n+        \"\"\"Get a new _Definition object with a setting removed.\"\"\"\n+        new = copy.copy(self)\n+        filtered_settings = tuple(s for s in self._settings if s.option != name)\n+        if len(filtered_settings) == len(self._settings):\n+            raise ValueError(f\"Setting {name} not found in {self}\")\n+        new._settings = filtered_settings  # pylint: disable=protected-access\n+        return new\n \n-# Our defaults for policy.images are different from Chromium's, so we mark it as\n-# mandatory setting - except on Qt 5.15.0 where we don't, so we don't get the\n-# workaround warning below if the setting wasn't explicitly customized.\n+    def copy_replace_setting(self, option: str, chromium_key: str) -&gt; '_Definition':\n+        \"\"\"Get a new _Definition object with `old` replaced by `new`.\n \n-_DEFINITIONS: MutableMapping[Variant, _Definition] = {\n-    Variant.qt_515_3: _Definition(\n-        # Different switch for settings\n-        _Setting('enabled', 'forceDarkModeEnabled', _BOOLS),\n-        _Setting('algorithm', 'InversionAlgorithm', _ALGORITHMS_NEW),\n+        If `old` is not in the settings list, raise ValueError.\n+        \"\"\"\n+        new = copy.deepcopy(self)\n \n-        _Setting('policy.images', 'ImagePolicy', _IMAGE_POLICIES),\n-        _Setting('contrast', 'ContrastPercent'),\n-        _Setting('grayscale.all', 'IsGrayScale', _BOOLS),\n+        for setting in new._settings:  # pylint: disable=protected-access\n+            if setting.option == option:\n+                setting.chromium_key = chromium_key\n+                return new\n \n-        _Setting('threshold.text', 'TextBrightnessThreshold'),\n-        _Setting('threshold.background', 'BackgroundBrightnessThreshold'),\n-        _Setting('grayscale.images', 'ImageGrayScalePercent'),\n+        raise ValueError(f\"Setting {option} not found in {self}\")\n \n-        mandatory={'enabled', 'policy.images'},\n-        prefix='',\n-        switch_names={'enabled': _BLINK_SETTINGS, None: 'dark-mode-settings'},\n-    ),\n \n-    # Qt 5.15.1 and 5.15.2 get added below, since there are only minor differences.\n+# Our defaults for policy.images are different from Chromium's, so we mark it as\n+# mandatory setting.\n \n-    Variant.qt_515_0: _Definition(\n-        # 'policy.images' not mandatory because it's broken\n+_DEFINITIONS: MutableMapping[Variant, _Definition] = {\n+    Variant.qt_515_2: _Definition(\n         _Setting('enabled', 'Enabled', _BOOLS),\n         _Setting('algorithm', 'InversionAlgorithm', _ALGORITHMS),\n \n         _Setting('policy.images', 'ImagePolicy', _IMAGE_POLICIES),\n         _Setting('contrast', 'Contrast'),\n-        _Setting('grayscale.all', 'Grayscale', _BOOLS),\n \n         _Setting('policy.page', 'PagePolicy', _PAGE_POLICIES),\n-        _Setting('threshold.text', 'TextBrightnessThreshold'),\n+        _Setting('threshold.foreground', 'TextBrightnessThreshold'),\n         _Setting('threshold.background', 'BackgroundBrightnessThreshold'),\n-        _Setting('grayscale.images', 'ImageGrayscale'),\n \n-        mandatory={'enabled'},\n-        prefix='darkMode',\n+        mandatory={'enabled', 'policy.images'},\n+        prefix='forceDarkMode',\n     ),\n \n-    Variant.qt_514: _Definition(\n-        _Setting('algorithm', '', _ALGORITHMS),  # new: kInvertLightnessLAB\n+    Variant.qt_515_3: _Definition(\n+        # Different switch for settings\n+        _Setting('enabled', 'forceDarkModeEnabled', _BOOLS),\n+        _Setting('algorithm', 'InversionAlgorithm', _ALGORITHMS_NEW),\n \n         _Setting('policy.images', 'ImagePolicy', _IMAGE_POLICIES),\n-        _Setting('contrast', 'Contrast'),\n-        _Setting('grayscale.all', 'Grayscale', _BOOLS),\n+        _Setting('contrast', 'ContrastPercent'),\n \n-        _Setting('policy.page', 'PagePolicy', _PAGE_POLICIES),\n-        _Setting('threshold.text', 'TextBrightnessThreshold'),\n+        _Setting('threshold.foreground', 'TextBrightnessThreshold'),\n         _Setting('threshold.background', 'BackgroundBrightnessThreshold'),\n-        _Setting('grayscale.images', 'ImageGrayscale'),\n \n-        mandatory={'algorithm', 'policy.images'},\n-        prefix='darkMode',\n-    ),\n-\n-    Variant.qt_511_to_513: _Definition(\n-        _Setting('algorithm', 'Mode', _ALGORITHMS_BEFORE_QT_514),\n-\n-        _Setting('policy.images', 'ImagePolicy', _IMAGE_POLICIES),\n-        _Setting('contrast', 'Contrast'),\n-        _Setting('grayscale.all', 'Grayscale', _BOOLS),\n-\n-        mandatory={'algorithm', 'policy.images'},\n-        prefix='highContrast',\n+        mandatory={'enabled', 'policy.images'},\n+        prefix='',\n+        switch_names={'enabled': _BLINK_SETTINGS, None: 'dark-mode-settings'},\n     ),\n }\n-_DEFINITIONS[Variant.qt_515_1] = (\n-    _DEFINITIONS[Variant.qt_515_0].copy_with('mandatory', {'enabled', 'policy.images'}))\n-_DEFINITIONS[Variant.qt_515_2] = (\n-    _DEFINITIONS[Variant.qt_515_1].copy_with('prefix', 'forceDarkMode'))\n-\n-\n-_PREFERRED_COLOR_SCHEME_DEFINITIONS = {\n-    # With older Qt versions, this is passed in qtargs.py as --force-dark-mode\n-    # instead.\n-\n-    ## Qt 5.15.2\n-    # 0: no-preference (not exposed)\n-    (Variant.qt_515_2, \"dark\"): \"1\",\n-    (Variant.qt_515_2, \"light\"): \"2\",\n-    # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-89753\n-    # Fall back to \"light\" instead of \"no preference\" (which was removed from the\n-    # standard)\n-    (Variant.qt_515_2, \"auto\"): \"2\",\n-    (Variant.qt_515_2, usertypes.UNSET): \"2\",\n-\n-    ## Qt &gt;= 5.15.3\n-    (Variant.qt_515_3, \"dark\"): \"0\",\n-    (Variant.qt_515_3, \"light\"): \"1\",\n+_DEFINITIONS[Variant.qt_64] = _DEFINITIONS[Variant.qt_515_3].copy_replace_setting(\n+    'threshold.foreground', 'ForegroundBrightnessThreshold',\n+)\n+_DEFINITIONS[Variant.qt_66] = _DEFINITIONS[Variant.qt_64].copy_add_setting(\n+    _Setting('policy.images', 'ImageClassifierPolicy', _IMAGE_CLASSIFIERS),\n+)\n+# Qt 6.7: Enabled is now handled dynamically via QWebEngineSettings\n+_DEFINITIONS[Variant.qt_67] = _DEFINITIONS[Variant.qt_66].copy_remove_setting('enabled')\n+\n+\n+_SettingValType = Union[str, usertypes.Unset]\n+_PREFERRED_COLOR_SCHEME_DEFINITIONS: MutableMapping[Variant, Mapping[_SettingValType, str]] = {\n+    Variant.qt_515_2: {\n+        # 0: no-preference (not exposed)\n+        \"dark\": \"1\",\n+        \"light\": \"2\",\n+        # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-89753\n+        # Fall back to \"light\" instead of \"no preference\" (which was removed from the\n+        # standard)\n+        \"auto\": \"2\",\n+        usertypes.UNSET: \"2\",\n+    },\n+\n+    Variant.qt_515_3: {\n+        \"dark\": \"0\",\n+        \"light\": \"1\",\n+    },\n }\n+for darkmode_variant in Variant:\n+    if darkmode_variant not in _PREFERRED_COLOR_SCHEME_DEFINITIONS:\n+        _PREFERRED_COLOR_SCHEME_DEFINITIONS[darkmode_variant] = \\\n+            _PREFERRED_COLOR_SCHEME_DEFINITIONS[Variant.qt_515_3]\n \n \n def _variant(versions: version.WebEngineVersions) -&gt; Variant:\n@@ -341,7 +373,18 @@ def _variant(versions: version.WebEngineVersions) -&gt; Variant:\n         except KeyError:\n             log.init.warning(f\"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}\")\n \n-    if (versions.webengine == utils.VersionNumber(5, 15, 2) and\n+    if (\n+        # We need a PyQt 6.7 as well with the API available, otherwise we can't turn on\n+        # dark mode later in webenginesettings.py.\n+        versions.webengine &gt;= utils.VersionNumber(6, 7) and\n+        hasattr(QWebEngineSettings.WebAttribute, 'ForceDarkMode')\n+    ):\n+        return Variant.qt_67\n+    elif versions.webengine &gt;= utils.VersionNumber(6, 6):\n+        return Variant.qt_66\n+    elif versions.webengine &gt;= utils.VersionNumber(6, 4):\n+        return Variant.qt_64\n+    elif (versions.webengine == utils.VersionNumber(5, 15, 2) and\n             versions.chromium_major == 87):\n         # WORKAROUND for Gentoo packaging something newer as 5.15.2...\n         return Variant.qt_515_3\n@@ -349,14 +392,6 @@ def _variant(versions: version.WebEngineVersions) -&gt; Variant:\n         return Variant.qt_515_3\n     elif versions.webengine &gt;= utils.VersionNumber(5, 15, 2):\n         return Variant.qt_515_2\n-    elif versions.webengine == utils.VersionNumber(5, 15, 1):\n-        return Variant.qt_515_1\n-    elif versions.webengine == utils.VersionNumber(5, 15):\n-        return Variant.qt_515_0\n-    elif versions.webengine &gt;= utils.VersionNumber(5, 14):\n-        return Variant.qt_514\n-    elif versions.webengine &gt;= utils.VersionNumber(5, 11):\n-        return Variant.qt_511_to_513\n     raise utils.Unreachable(versions.webengine)\n \n \n@@ -387,12 +422,11 @@ def settings(\n                 key, val = pair.split('=', maxsplit=1)\n                 result[_BLINK_SETTINGS].append((key, val))\n \n-    preferred_color_scheme_key = (\n-        variant,\n-        config.instance.get(\"colors.webpage.preferred_color_scheme\", fallback=False),\n-    )\n-    if preferred_color_scheme_key in _PREFERRED_COLOR_SCHEME_DEFINITIONS:\n-        value = _PREFERRED_COLOR_SCHEME_DEFINITIONS[preferred_color_scheme_key]\n+    preferred_color_scheme_key = config.instance.get(\n+        \"colors.webpage.preferred_color_scheme\", fallback=False)\n+    preferred_color_scheme_defs = _PREFERRED_COLOR_SCHEME_DEFINITIONS[variant]\n+    if preferred_color_scheme_key in preferred_color_scheme_defs:\n+        value = preferred_color_scheme_defs[preferred_color_scheme_key]\n         result[_BLINK_SETTINGS].append((\"preferredColorScheme\", value))\n \n     if not config.val.colors.webpage.darkmode.enabled:\n@@ -411,14 +445,8 @@ def settings(\n         if isinstance(value, usertypes.Unset):\n             continue\n \n-        if (setting.option == 'policy.images' and value == 'smart' and\n-                variant == Variant.qt_515_0):\n-            # WORKAROUND for\n-            # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211\n-            log.init.warning(\"Ignoring colors.webpage.darkmode.policy.images = smart \"\n-                             \"because of Qt 5.15.0 bug\")\n-            continue\n-\n-        result[switch_name].append(setting.chromium_tuple(value))\n+        chromium_tuple = setting.chromium_tuple(value)\n+        if chromium_tuple is not None:\n+            result[switch_name].append(chromium_tuple)\n \n     return result\ndiff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py\nindex 80c5c29c4..161f5ffab 100644\n--- a/qutebrowser/browser/webengine/interceptor.py\n+++ b/qutebrowser/browser/webengine/interceptor.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"A request interceptor taking care of adblocking and custom headers.\"\"\"\n \n@@ -25,7 +10,7 @@ from qutebrowser.qt.webenginecore import (QWebEngineUrlRequestInterceptor,\n \n from qutebrowser.config import websettings, config\n from qutebrowser.browser import shared\n-from qutebrowser.utils import utils, log, debug, qtutils\n+from qutebrowser.utils import debug, log, qtutils\n from qutebrowser.extensions import interceptors\n from qutebrowser.misc import objects\n \n@@ -34,7 +19,10 @@ class WebEngineRequest(interceptors.Request):\n \n     \"\"\"QtWebEngine-specific request interceptor functionality.\"\"\"\n \n-    _WHITELISTED_REQUEST_METHODS = {QByteArray(b'GET'), QByteArray(b'HEAD')}\n+    _WHITELISTED_REQUEST_METHODS = {\n+        QByteArray(b'GET'),\n+        QByteArray(b'HEAD'),\n+    }\n \n     def __init__(self, *args, webengine_info, **kwargs):\n         super().__init__(*args, **kwargs)\n@@ -47,6 +35,11 @@ class WebEngineRequest(interceptors.Request):\n         if self._webengine_info is None:\n             raise interceptors.RedirectException(\"Request improperly initialized.\")\n \n+        try:\n+            qtutils.ensure_valid(url)\n+        except qtutils.QtValueError as e:\n+            raise interceptors.RedirectException(f\"Redirect to invalid URL: {e}\")\n+\n         # Redirecting a request that contains payload data is not allowed.\n         # To be safe, abort on any request not in a whitelist.\n         verb = self._webengine_info.requestMethod()\n@@ -109,44 +102,33 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):\n                 interceptors.ResourceType.plugin_resource,\n             QWebEngineUrlRequestInfo.ResourceType.ResourceTypeUnknown:\n                 interceptors.ResourceType.unknown,\n+            QWebEngineUrlRequestInfo.ResourceType.ResourceTypeNavigationPreloadMainFrame:\n+                interceptors.ResourceType.preload_main_frame,\n+            QWebEngineUrlRequestInfo.ResourceType.ResourceTypeNavigationPreloadSubFrame:\n+                interceptors.ResourceType.preload_sub_frame,\n         }\n-\n-        try:\n-            preload_main_frame = (QWebEngineUrlRequestInfo.\n-                                  ResourceType.\n-                                  ResourceTypeNavigationPreloadMainFrame)\n-            preload_sub_frame = (QWebEngineUrlRequestInfo.\n-                                 ResourceType.\n-                                 ResourceTypeNavigationPreloadSubFrame)\n-        except AttributeError:\n-            # Added in Qt 5.14\n-            pass\n-        else:\n-            self._resource_types[preload_main_frame] = (\n-                interceptors.ResourceType.preload_main_frame)\n-            self._resource_types[preload_sub_frame] = (\n-                interceptors.ResourceType.preload_sub_frame)\n+        new_types = {\n+            \"WebSocket\": interceptors.ResourceType.websocket,  # added in Qt 6.4\n+        }\n+        for qt_name, qb_value in new_types.items():\n+            qt_value = getattr(\n+                QWebEngineUrlRequestInfo.ResourceType,\n+                f\"ResourceType{qt_name}\",\n+                None,\n+            )\n+            if qt_value is not None:\n+                self._resource_types[qt_value] = qb_value\n \n     def install(self, profile):\n         \"\"\"Install the interceptor on the given QWebEngineProfile.\"\"\"\n-        try:\n-            # Qt &gt;= 5.13, GUI thread\n-            profile.setUrlRequestInterceptor(self)\n-        except AttributeError:\n-            # Qt 5.12, IO thread\n-            profile.setRequestInterceptor(self)\n-\n-    # Gets called in the IO thread -&gt; showing crash window will fail\n-    @utils.prevent_exceptions(None, not qtutils.version_check('5.13'))\n+        profile.setUrlRequestInterceptor(self)\n+\n     def interceptRequest(self, info):\n         \"\"\"Handle the given request.\n \n         Reimplementing this virtual function and setting the interceptor on a\n         profile makes it possible to intercept URL requests.\n \n-        On Qt &lt; 5.13, this function is executed on the IO thread, and therefore\n-        running long tasks here will block networking.\n-\n         info contains the information about the URL request and will track\n         internally whether its members have been altered.\n \n@@ -216,9 +198,6 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):\n                 continue\n             info.setHttpHeader(header, value)\n \n-        # Note this is ignored before Qt 5.12.4 and 5.13.1 due to\n-        # https://bugreports.qt.io/browse/QTBUG-60203 - there, we set the\n-        # commandline-flag in qtargs.py instead.\n         if config.cache['content.headers.referer'] == 'never':\n             info.setHttpHeader(b'Referer', b'')\n \ndiff --git a/qutebrowser/browser/webengine/notification.py b/qutebrowser/browser/webengine/notification.py\nindex fb946a91a..e8b2e27f1 100644\n--- a/qutebrowser/browser/webengine/notification.py\n+++ b/qutebrowser/browser/webengine/notification.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2020 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Different ways of showing notifications to the user.\n \n@@ -50,6 +35,7 @@ import functools\n import subprocess\n from typing import Any, List, Dict, Optional, Iterator, Type, TYPE_CHECKING\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import (Qt, QObject, QVariant, QMetaType, QByteArray, pyqtSlot,\n                           pyqtSignal, QTimer, QProcess, QUrl)\n from qutebrowser.qt.gui import QImage, QIcon, QPixmap\n@@ -60,13 +46,12 @@ from qutebrowser.qt.widgets import QSystemTrayIcon\n if TYPE_CHECKING:\n     # putting these behind TYPE_CHECKING also means this module is importable\n     # on installs that don't have these\n-    from qutebrowser.qt.webenginecore import QWebEngineNotification\n-    from qutebrowser.qt.webenginewidgets import QWebEngineProfile\n+    from qutebrowser.qt.webenginecore import QWebEngineNotification, QWebEngineProfile\n \n from qutebrowser.config import config\n from qutebrowser.misc import objects\n from qutebrowser.utils import (\n-    qtutils, log, utils, debug, message, version, objreg, resources,\n+    qtutils, log, utils, debug, message, objreg, resources, urlutils\n )\n from qutebrowser.qt import sip\n \n@@ -74,12 +59,6 @@ from qutebrowser.qt import sip\n bridge: Optional['NotificationBridgePresenter'] = None\n \n \n-def _notifications_supported() -&gt; bool:\n-    \"\"\"Check whether the current QtWebEngine version has notification support.\"\"\"\n-    versions = version.qtwebengine_versions(avoid_init=True)\n-    return versions.webengine &gt;= utils.VersionNumber(5, 14)\n-\n-\n def init() -&gt; None:\n     \"\"\"Initialize the DBus notification presenter, if applicable.\n \n@@ -94,9 +73,6 @@ def init() -&gt; None:\n         # to its usefulness.\n         return\n \n-    if not _notifications_supported():\n-        return\n-\n     global bridge\n     bridge = NotificationBridgePresenter()\n \n@@ -133,6 +109,13 @@ class DBusError(Error):\n         # https://crashes.qutebrowser.org/view/8889d0b5\n         # Could not activate remote peer.\n         \"org.freedesktop.DBus.Error.NameHasNoOwner\",\n+\n+        # https://crashes.qutebrowser.org/view/de62220a\n+        # after \"Notification daemon did quit!\"\n+        \"org.freedesktop.DBus.Error.UnknownObject\",\n+\n+        # notmuch-sha1-ef7b6e9e79e5f2f6cba90224122288895c1fe0d8\n+        \"org.freedesktop.DBus.Error.ServiceUnknown\",\n     }\n \n     def __init__(self, msg: QDBusMessage) -&gt; None:\n@@ -204,7 +187,6 @@ class NotificationBridgePresenter(QObject):\n     \"\"\"Notification presenter which bridges notifications to an adapter.\n \n     Takes care of:\n-    - Working around bugs in PyQt 5.14\n     - Storing currently shown notifications, using an ID returned by the adapter.\n     - Initializing a suitable adapter when the first notification is shown.\n     - Switching out adapters if the current one emitted its error signal.\n@@ -212,7 +194,6 @@ class NotificationBridgePresenter(QObject):\n \n     def __init__(self, parent: QObject = None) -&gt; None:\n         super().__init__(parent)\n-        assert _notifications_supported()\n \n         self._active_notifications: Dict[int, 'QWebEngineNotification'] = {}\n         self._adapter: Optional[AbstractNotificationAdapter] = None\n@@ -276,24 +257,7 @@ class NotificationBridgePresenter(QObject):\n \n     def install(self, profile: \"QWebEngineProfile\") -&gt; None:\n         \"\"\"Set the profile to use this bridge as the presenter.\"\"\"\n-        # WORKAROUND for\n-        # https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042916.html\n-        # Fixed in PyQtWebEngine 5.15.0\n-        # PYQT_WEBENGINE_VERSION was added with PyQtWebEngine 5.13, but if we're here,\n-        # we already did a version check above.\n-        from qutebrowser.qt.webenginecore import PYQT_WEBENGINE_VERSION\n-        if PYQT_WEBENGINE_VERSION &lt; 0x050F00:\n-            # PyQtWebEngine unrefs the callback after it's called, for some\n-            # reason.  So we call setNotificationPresenter again to *increase*\n-            # its refcount to prevent it from getting GC'd. Otherwise, random\n-            # methods start getting called with the notification as `self`, or\n-            # segfaults happen, or other badness.\n-            def _present_and_reset(qt_notification: \"QWebEngineNotification\") -&gt; None:\n-                profile.setNotificationPresenter(_present_and_reset)\n-                self.present(qt_notification)\n-            profile.setNotificationPresenter(_present_and_reset)\n-        else:\n-            profile.setNotificationPresenter(self.present)\n+        profile.setNotificationPresenter(self.present)\n \n     def present(self, qt_notification: \"QWebEngineNotification\") -&gt; None:\n         \"\"\"Show a notification using the configured adapter.\n@@ -345,18 +309,11 @@ class NotificationBridgePresenter(QObject):\n             f\"Finding notification for tag {new_notification.tag()}, \"\n             f\"origin {new_notification.origin()}\")\n \n-        try:\n-            for notification_id, notification in sorted(\n-                    self._active_notifications.items(), reverse=True):\n-                if notification.matches(new_notification):\n-                    log.misc.debug(f\"Found match: {notification_id}\")\n-                    return notification_id\n-        except RuntimeError:\n-            # WORKAROUND for\n-            # https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042918.html\n-            # (also affects .matches)\n-            log.misc.debug(\n-                f\"Ignoring notification tag {new_notification.tag()!r} due to PyQt bug\")\n+        for notification_id, notification in sorted(\n+                self._active_notifications.items(), reverse=True):\n+            if notification.matches(new_notification):\n+                log.misc.debug(f\"Found match: {notification_id}\")\n+                return notification_id\n \n         log.misc.debug(\"Did not find match\")\n         return None\n@@ -377,13 +334,7 @@ class NotificationBridgePresenter(QObject):\n             # Notification from a different application\n             return\n \n-        try:\n-            notification.close()\n-        except RuntimeError:\n-            # WORKAROUND for\n-            # https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042918.html\n-            log.misc.debug(f\"Ignoring close request for notification {notification_id} \"\n-                           \"due to PyQt bug\")\n+        notification.close()\n \n     @pyqtSlot(int)\n     def _on_adapter_clicked(self, notification_id: int) -&gt; None:\n@@ -401,14 +352,7 @@ class NotificationBridgePresenter(QObject):\n             log.misc.debug(\"Did not find matching notification, ignoring\")\n             return\n \n-        try:\n-            notification.click()\n-        except RuntimeError:\n-            # WORKAROUND for\n-            # https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042918.html\n-            log.misc.debug(f\"Ignoring click request for notification {notification_id} \"\n-                           \"due to PyQt bug\")\n-            return\n+        notification.click()\n         self._focus_first_matching_tab(notification)\n \n     def _focus_first_matching_tab(self, notification: \"QWebEngineNotification\") -&gt; None:\n@@ -679,8 +623,8 @@ class HerbeNotificationAdapter(AbstractNotificationAdapter):\n     def _on_error(self, error: QProcess.ProcessError) -&gt; None:\n         if error == QProcess.ProcessError.Crashed:\n             return\n-        name = debug.qenum_key(QProcess.ProcessError, error)\n-        raise Error(f'herbe process error: {name}')\n+        name = debug.qenum_key(QProcess, error)\n+        self.error.emit(f'herbe process error: {name}')\n \n     @pyqtSlot(int)\n     def on_web_closed(self, notification_id: int) -&gt; None:\n@@ -733,7 +677,14 @@ class _ServerCapabilities:\n def _as_uint32(x: int) -&gt; QVariant:\n     \"\"\"Convert the given int to an uint32 for DBus.\"\"\"\n     variant = QVariant(x)\n-    successful = variant.convert(QVariant.Type.UInt)\n+\n+    if machinery.IS_QT5:\n+        target = QVariant.Type.UInt\n+    else:  # Qt 6\n+        # FIXME:mypy PyQt6-stubs issue\n+        target = QMetaType(QMetaType.Type.UInt.value)  # type: ignore[call-overload]\n+\n+    successful = variant.convert(target)\n     assert successful\n     return variant\n \n@@ -759,7 +710,6 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):\n \n     def __init__(self, parent: QObject = None) -&gt; None:\n         super().__init__(parent)\n-        assert _notifications_supported()\n \n         if utils.is_windows:\n             # The QDBusConnection destructor seems to cause error messages (and\n@@ -851,7 +801,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):\n             # https://github.com/sboli/twmn/pull/96\n             return _ServerQuirks(spec_version=\"0\")\n         elif (name, vendor) == (\"tiramisu\", \"Sweets\"):\n-            if utils.VersionNumber.parse(ver) &lt; utils.VersionNumber(2, 0):\n+            if utils.VersionNumber.parse(ver) &lt; utils.VersionNumber(2):\n                 # https://github.com/Sweets/tiramisu/issues/20\n                 return _ServerQuirks(skip_capabilities=True)\n         elif (name, vendor) == (\"lxqt-notificationd\", \"lxqt.org\"):\n@@ -909,12 +859,15 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):\n             log.misc.debug(f\"Enabling quirks {quirks}\")\n             self._quirks = quirks\n \n-        expected_spec_version = self._quirks.spec_version or self.SPEC_VERSION\n-        if spec_version != expected_spec_version:\n+        expected_spec_versions = [self.SPEC_VERSION]\n+        if self._quirks.spec_version is not None:\n+            expected_spec_versions.append(self._quirks.spec_version)\n+\n+        if spec_version not in expected_spec_versions:\n             log.misc.warning(\n                 f\"Notification server ({name} {ver} by {vendor}) implements \"\n-                f\"spec {spec_version}, but {expected_spec_version} was expected. \"\n-                f\"If {name} is up to date, please report a qutebrowser bug.\")\n+                f\"spec {spec_version}, but {'/'.join(expected_spec_versions)} was \"\n+                f\"expected. If {name} is up to date, please report a qutebrowser bug.\")\n \n         # https://specifications.freedesktop.org/notification-spec/latest/ar01s08.html\n         icon_key_overrides = {\n@@ -956,8 +909,8 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):\n \n         typ = msg.type()\n         if typ != expected_type:\n-            type_str = debug.qenum_key(QDBusMessage.MessageType, typ)\n-            expected_type_str = debug.qenum_key(QDBusMessage.MessageType, expected_type)\n+            type_str = debug.qenum_key(QDBusMessage, typ)\n+            expected_type_str = debug.qenum_key(QDBusMessage, expected_type)\n             raise Error(\n                 f\"Got a message of type {type_str} but expected {expected_type_str}\"\n                 f\"(args: {msg.arguments()})\")\n@@ -993,7 +946,10 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):\n         actions = []\n         if self._capabilities.actions:\n             actions = ['default', 'Activate']  # key, name\n-        return QDBusArgument(actions, QMetaType.Type.QStringList)\n+        return QDBusArgument(\n+            actions,\n+            qtutils.extract_enum_val(QMetaType.Type.QStringList),\n+        )\n \n     def _get_hints_arg(self, *, origin_url: QUrl, icon: QImage) -&gt; Dict[str, Any]:\n         \"\"\"Get the hints argument for present().\"\"\"\n@@ -1113,14 +1069,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):\n         image_data.add(bits_per_color)\n         image_data.add(channel_count)\n \n-        try:\n-            size = qimage.sizeInBytes()\n-        except TypeError:\n-            # WORKAROUND for\n-            # https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042919.html\n-            # byteCount() is obsolete, but sizeInBytes() is only available with\n-            # SIP &gt;= 5.3.0.\n-            size = qimage.byteCount()\n+        size = qimage.sizeInBytes()\n \n         # Despite the spec not mandating this, many notification daemons mandate that\n         # the last scanline does not have any padding bytes.\n@@ -1152,7 +1101,9 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):\n         if padding and self._quirks.no_padded_images:\n             return None\n \n-        bits = qimage.constBits().asstring(size)\n+        bits_ptr = qimage.constBits()\n+        assert bits_ptr is not None\n+        bits = bits_ptr.asstring(size)\n         image_data.add(QByteArray(bits))\n \n         image_data.endStructure()\n@@ -1220,9 +1171,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):\n         if self._capabilities.kde_origin_name or not is_useful_origin:\n             prefix = None\n         elif self._capabilities.body_markup and self._capabilities.body_hyperlinks:\n-            href = html.escape(\n-                origin_url.toString(QUrl.ComponentFormattingOption.FullyEncoded)  # type: ignore[arg-type]\n-            )\n+            href = html.escape(origin_url.toString(urlutils.FormatOption.ENCODED))\n             text = html.escape(urlstr, quote=False)\n             prefix = f'{text}'\n         elif self._capabilities.body_markup:\ndiff --git a/qutebrowser/browser/webengine/spell.py b/qutebrowser/browser/webengine/spell.py\nindex 06c613d9b..d1e921432 100644\n--- a/qutebrowser/browser/webengine/spell.py\n+++ b/qutebrowser/browser/webengine/spell.py\n@@ -1,22 +1,8 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-# Copyright 2017-2018 Michal Siedlaczek \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Michal Siedlaczek \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Installing and configuring spell-checking for QtWebEngine.\"\"\"\n \ndiff --git a/qutebrowser/browser/webengine/tabhistory.py b/qutebrowser/browser/webengine/tabhistory.py\nindex 6efc6a2aa..340fab550 100644\n--- a/qutebrowser/browser/webengine/tabhistory.py\n+++ b/qutebrowser/browser/webengine/tabhistory.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"QWebHistory serializer for QtWebEngine.\"\"\"\n \n@@ -155,6 +140,8 @@ def serialize(items):\n     for item in items:\n         _serialize_item(item, stream)\n \n-    stream.device().reset()\n+    dev = stream.device()\n+    assert dev is not None\n+    dev.reset()\n     qtutils.check_qdatastream(stream)\n     return stream, data, cur_user_data\ndiff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py\nindex 868e30f6e..e8e6418e0 100644\n--- a/qutebrowser/browser/webengine/webenginedownloads.py\n+++ b/qutebrowser/browser/webengine/webenginedownloads.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"QtWebEngine specific code for downloads.\"\"\"\n \n@@ -23,6 +8,7 @@ import re\n import os.path\n import functools\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import pyqtSlot, Qt, QUrl, QObject\n from qutebrowser.qt.webenginecore import QWebEngineDownloadRequest\n \n@@ -44,8 +30,23 @@ class DownloadItem(downloads.AbstractDownloadItem):\n                  parent: QObject = None) -&gt; None:\n         super().__init__(manager=manager, parent=manager)\n         self._qt_item = qt_item\n-        qt_item.downloadProgress.connect(self.stats.on_download_progress)\n-        qt_item.stateChanged.connect(self._on_state_changed)\n+        if machinery.IS_QT5:\n+            qt_item.downloadProgress.connect(self.stats.on_download_progress)\n+        else:  # Qt 6\n+            qt_item.receivedBytesChanged.connect(\n+                lambda: self.stats.on_download_progress(\n+                    qt_item.receivedBytes(),\n+                    qt_item.totalBytes(),\n+                )\n+            )\n+            qt_item.totalBytesChanged.connect(\n+                lambda: self.stats.on_download_progress(\n+                    qt_item.receivedBytes(),\n+                    qt_item.totalBytes(),\n+                )\n+            )\n+        qt_item.stateChanged.connect(\n+            self._on_state_changed)\n \n         # Ensure wrapped qt_item is deleted manually when the wrapper object\n         # is deleted. See https://github.com/qutebrowser/qutebrowser/issues/3373\n@@ -89,8 +90,12 @@ class DownloadItem(downloads.AbstractDownloadItem):\n                              \"{}\".format(state_name))\n \n     def _do_die(self):\n-        progress_signal = self._qt_item.downloadProgress\n-        progress_signal.disconnect()\n+        if machinery.IS_QT5:\n+            self._qt_item.downloadProgress.disconnect()\n+        else:  # Qt 6\n+            self._qt_item.receivedBytesChanged.disconnect()\n+            self._qt_item.totalBytesChanged.disconnect()\n+\n         if self._qt_item.state() != QWebEngineDownloadRequest.DownloadState.DownloadInterrupted:\n             self._qt_item.cancel()\n \n@@ -174,12 +179,8 @@ class DownloadItem(downloads.AbstractDownloadItem):\n         assert self._filename is not None\n \n         dirname, basename = os.path.split(self._filename)\n-        try:\n-            # Qt 5.14\n-            self._qt_item.setDownloadDirectory(dirname)\n-            self._qt_item.setDownloadFileName(basename)\n-        except AttributeError:\n-            self._qt_item.setPath(self._filename)\n+        self._qt_item.setDownloadDirectory(dirname)\n+        self._qt_item.setDownloadFileName(basename)\n \n         self._qt_item.accept()\n \n@@ -250,7 +251,7 @@ class DownloadManager(downloads.AbstractDownloadManager):\n     @pyqtSlot(QWebEngineDownloadRequest)\n     def handle_download(self, qt_item):\n         \"\"\"Start a download coming from a QWebEngineProfile.\"\"\"\n-        qt_filename = os.path.basename(qt_item.path())   # FIXME use 5.14 API\n+        qt_filename = qt_item.downloadFileName()\n         mime_type = qt_item.mimeType()\n         url = qt_item.url()\n \ndiff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py\nindex 08914af53..c387ebbcf 100644\n--- a/qutebrowser/browser/webengine/webengineelem.py\n+++ b/qutebrowser/browser/webengine/webengineelem.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"QtWebEngine specific part of the web element API.\"\"\"\n \n@@ -26,7 +11,7 @@ from qutebrowser.qt.core import QRect, QEventLoop\n from qutebrowser.qt.widgets import QApplication\n from qutebrowser.qt.webenginecore import QWebEngineSettings\n \n-from qutebrowser.utils import log, javascript, urlutils, usertypes, utils\n+from qutebrowser.utils import log, javascript, urlutils, usertypes, utils, version\n from qutebrowser.browser import webelem\n \n if TYPE_CHECKING:\n@@ -228,10 +213,22 @@ class WebEngineElement(webelem.AbstractWebElement):\n             return True\n         if baseurl.scheme() == url.scheme():  # e.g. a qute:// link\n             return False\n+\n+        # Qt 6.3+ needs a user interaction to allow navigations from qute:// to\n+        # outside qute:// (like e.g. on qute://bookmarks), as well as from file:// to\n+        # outside of file:// (e.g. users having a local bookmarks.html).\n+        versions = version.qtwebengine_versions()\n+        for scheme in [\"qute\", \"file\"]:\n+            if (\n+                baseurl.scheme() == scheme and\n+                url.scheme() != scheme and\n+                versions.webengine &gt;= utils.VersionNumber(6, 3)\n+            ):\n+                return True\n+\n         return url.scheme() not in urlutils.WEBENGINE_SCHEMES\n \n     def _click_editable(self, click_target: usertypes.ClickTarget) -&gt; None:\n-        self._tab.setFocus()  # Needed as WORKAROUND on Qt 5.12\n         # This actually \"clicks\" the element by calling focus() on it in JS.\n         self._js_call('focus')\n         self._move_text_cursor()\ndiff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py\nindex 6f8d574fd..d37f41ba5 100644\n--- a/qutebrowser/browser/webengine/webengineinspector.py\n+++ b/qutebrowser/browser/webengine/webengineinspector.py\n@@ -1,30 +1,18 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Customized QWebInspector for QtWebEngine.\"\"\"\n \n+from typing import Optional\n+\n+from qutebrowser.qt import machinery\n from qutebrowser.qt.webenginewidgets import QWebEngineView\n from qutebrowser.qt.webenginecore import QWebEnginePage\n from qutebrowser.qt.widgets import QWidget\n \n from qutebrowser.browser import inspector\n-from qutebrowser.browser.webengine import webenginesettings\n+from qutebrowser.browser.webengine import webenginesettings, webview\n from qutebrowser.misc import miscwidgets\n from qutebrowser.utils import version, usertypes, qtutils\n from qutebrowser.keyinput import modeman\n@@ -47,9 +35,19 @@ class WebEngineInspectorView(QWebEngineView):\n \n         See WebEngineView.createWindow for details.\n         \"\"\"\n-        view = self.page().inspectedPage().view()\n-        assert isinstance(view, QWebEngineView), view\n-        return view.createWindow(wintype)\n+        our_page = self.page()\n+        assert our_page is not None\n+        inspected_page = our_page.inspectedPage()\n+        assert inspected_page is not None\n+        if machinery.IS_QT5:\n+            view = inspected_page.view()\n+            assert isinstance(view, QWebEngineView), view\n+            return view.createWindow(wintype)\n+        else:  # Qt 6\n+            newpage = inspected_page.createWindow(wintype)\n+            ret = webview.WebEngineView.forPage(newpage)\n+            assert ret is not None\n+            return ret\n \n \n class WebEngineInspector(inspector.AbstractWebInspector):\n@@ -63,12 +61,7 @@ class WebEngineInspector(inspector.AbstractWebInspector):\n                  parent: QWidget = None) -&gt; None:\n         super().__init__(splitter, win_id, parent)\n         self._check_devtools_resources()\n-\n-        view = WebEngineInspectorView()\n-        self._settings = webenginesettings.WebEngineSettings(view.settings())\n-        self._set_widget(view)\n-        page = view.page()\n-        page.windowCloseRequested.connect(self._on_window_close_requested)\n+        self._settings: Optional[webenginesettings.WebEngineSettings] = None\n \n     def _on_window_close_requested(self) -&gt; None:\n         \"\"\"Called when the 'x' was clicked in the devtools.\"\"\"\n@@ -98,8 +91,23 @@ class WebEngineInspector(inspector.AbstractWebInspector):\n                                   \"Fedora package.\")\n \n     def inspect(self, page: QWebEnginePage) -&gt; None:\n+        if not self._widget:\n+            view = WebEngineInspectorView()\n+            new_page = QWebEnginePage(\n+                page.profile(),\n+                self\n+            )\n+            new_page.windowCloseRequested.connect(self._on_window_close_requested)\n+            view.setPage(new_page)\n+            self._settings = webenginesettings.WebEngineSettings(view.settings())\n+            self._set_widget(view)\n+\n         inspector_page = self._widget.page()\n+        assert inspector_page is not None\n+        assert inspector_page.profile() == page.profile()\n         inspector_page.setInspectedPage(page)\n+\n+        assert self._settings is not None\n         self._settings.update_for_url(inspector_page.requestedUrl())\n \n     def _needs_recreate(self) -&gt; bool:\ndiff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py\nindex 6fb809f6d..5d617f87b 100644\n--- a/qutebrowser/browser/webengine/webenginequtescheme.py\n+++ b/qutebrowser/browser/webengine/webenginequtescheme.py\n@@ -1,25 +1,10 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"QtWebEngine specific qute://* handlers and glue code.\"\"\"\n \n-from qutebrowser.qt.core import QBuffer, QIODevice, QUrl\n+from qutebrowser.qt.core import QBuffer, QIODevice, QUrl, QByteArray\n from qutebrowser.qt.webenginecore import (QWebEngineUrlSchemeHandler,\n                                    QWebEngineUrlRequestJob,\n                                    QWebEngineUrlScheme)\n@@ -27,6 +12,8 @@ from qutebrowser.qt.webenginecore import (QWebEngineUrlSchemeHandler,\n from qutebrowser.browser import qutescheme\n from qutebrowser.utils import log, qtutils\n \n+_QUTE = QByteArray(b'qute')\n+\n \n class QuteSchemeHandler(QWebEngineUrlSchemeHandler):\n \n@@ -35,9 +22,9 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):\n     def install(self, profile):\n         \"\"\"Install the handler for qute:// URLs on the given profile.\"\"\"\n         if QWebEngineUrlScheme is not None:\n-            assert QWebEngineUrlScheme.schemeByName(b'qute') is not None\n+            assert QWebEngineUrlScheme.schemeByName(_QUTE) is not None\n \n-        profile.installUrlSchemeHandler(b'qute', self)\n+        profile.installUrlSchemeHandler(_QUTE, self)\n \n     def _check_initiator(self, job):\n         \"\"\"Check whether the initiator of the job should be allowed.\n@@ -135,8 +122,8 @@ def init():\n     classes.\n     \"\"\"\n     if QWebEngineUrlScheme is not None:\n-        assert not QWebEngineUrlScheme.schemeByName(b'qute').name()\n-        scheme = QWebEngineUrlScheme(b'qute')\n+        assert not QWebEngineUrlScheme.schemeByName(_QUTE).name()\n+        scheme = QWebEngineUrlScheme(_QUTE)\n         scheme.setFlags(\n             QWebEngineUrlScheme.Flag.LocalScheme |\n             QWebEngineUrlScheme.Flag.LocalAccessAllowed)\ndiff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py\nindex a853a03bd..fd0d8c8de 100644\n--- a/qutebrowser/browser/webengine/webenginesettings.py\n+++ b/qutebrowser/browser/webengine/webenginesettings.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Bridge from QWebEngineSettings to our own settings.\n \n@@ -26,8 +11,10 @@ Module attributes:\n \n import os\n import operator\n+import pathlib\n from typing import cast, Any, List, Optional, Tuple, Union, TYPE_CHECKING\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.gui import QFont\n from qutebrowser.qt.widgets import QApplication\n from qutebrowser.qt.webenginecore import QWebEngineSettings, QWebEngineProfile\n@@ -37,6 +24,7 @@ from qutebrowser.browser.webengine import (spell, webenginequtescheme, cookies,\n                                            webenginedownloads, notification)\n from qutebrowser.config import config, websettings\n from qutebrowser.config.websettings import AttributeInfo as Attr\n+from qutebrowser.misc import pakjoy\n from qutebrowser.utils import (standarddir, qtutils, message, log,\n                                urlmatch, usertypes, objreg, version)\n if TYPE_CHECKING:\n@@ -49,7 +37,7 @@ private_profile: Optional[QWebEngineProfile] = None\n # The global WebEngineSettings object\n _global_settings = cast('WebEngineSettings', None)\n \n-parsed_user_agent = None\n+parsed_user_agent: Optional[websettings.UserAgent] = None\n \n _qute_scheme_handler = cast(webenginequtescheme.QuteSchemeHandler, None)\n _req_interceptor = cast('interceptor.RequestInterceptor', None)\n@@ -63,8 +51,12 @@ class _SettingsWrapper:\n     For read operations, the default profile value is always used.\n     \"\"\"\n \n+    def _default_profile_settings(self):\n+        assert default_profile is not None\n+        return default_profile.settings()\n+\n     def _settings(self):\n-        yield default_profile.settings()\n+        yield self._default_profile_settings()\n         if private_profile:\n             yield private_profile.settings()\n \n@@ -89,19 +81,19 @@ class _SettingsWrapper:\n             settings.setUnknownUrlSchemePolicy(policy)\n \n     def testAttribute(self, attribute):\n-        return default_profile.settings().testAttribute(attribute)\n+        return self._default_profile_settings().testAttribute(attribute)\n \n     def fontSize(self, fonttype):\n-        return default_profile.settings().fontSize(fonttype)\n+        return self._default_profile_settings().fontSize(fonttype)\n \n     def fontFamily(self, which):\n-        return default_profile.settings().fontFamily(which)\n+        return self._default_profile_settings().fontFamily(which)\n \n     def defaultTextEncoding(self):\n-        return default_profile.settings().defaultTextEncoding()\n+        return self._default_profile_settings().defaultTextEncoding()\n \n     def unknownUrlSchemePolicy(self):\n-        return default_profile.settings().unknownUrlSchemePolicy()\n+        return self._default_profile_settings().unknownUrlSchemePolicy()\n \n \n class WebEngineSettings(websettings.AbstractSettings):\n@@ -157,6 +149,20 @@ class WebEngineSettings(websettings.AbstractSettings):\n                  converter=lambda val: val != 'never'),\n     }\n \n+    if machinery.IS_QT6:\n+        try:\n+            _ATTRIBUTES['content.canvas_reading'] = Attr(\n+                QWebEngineSettings.WebAttribute.ReadingFromCanvasEnabled)\n+        except AttributeError:\n+            # Added in QtWebEngine 6.6\n+            pass\n+        try:\n+            _ATTRIBUTES['colors.webpage.darkmode.enabled'] = Attr(\n+                QWebEngineSettings.WebAttribute.ForceDarkMode)\n+        except AttributeError:\n+            # Added in QtWebEngine 6.7\n+            pass\n+\n     _FONT_SIZES = {\n         'fonts.web.size.minimum':\n             QWebEngineSettings.FontSize.MinimumFontSize,\n@@ -256,15 +262,10 @@ class ProfileSetter:\n             'content.cache.size': self.set_http_cache_size,\n             'content.cookies.store': self.set_persistent_cookie_policy,\n             'spellcheck.languages': self.set_dictionary_language,\n+            'content.headers.user_agent': self.set_http_headers,\n+            'content.headers.accept_language': self.set_http_headers,\n         }\n \n-        # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-75884\n-        # (note this isn't actually fixed properly before Qt 5.15)\n-        header_bug_fixed = qtutils.version_check('5.15', compiled=False)\n-        if header_bug_fixed:\n-            for name in ['user_agent', 'accept_language']:\n-                self._name_to_method[f'content.headers.{name}'] = self.set_http_headers\n-\n     def update_setting(self, name):\n         \"\"\"Update a setting based on its name.\"\"\"\n         try:\n@@ -289,12 +290,7 @@ class ProfileSetter:\n             QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, True)\n         settings.setAttribute(\n             QWebEngineSettings.WebAttribute.FocusOnNavigationEnabled, False)\n-\n-        try:\n-            settings.setAttribute(QWebEngineSettings.WebAttribute.PdfViewerEnabled, False)\n-        except AttributeError:\n-            # Added in Qt 5.13\n-            pass\n+        settings.setAttribute(QWebEngineSettings.WebAttribute.PdfViewerEnabled, False)\n \n     def set_http_headers(self):\n         \"\"\"Set the user agent and accept-language for the given profile.\n@@ -364,7 +360,10 @@ def _init_user_agent_str(ua):\n \n \n def init_user_agent():\n-    _init_user_agent_str(QWebEngineProfile.defaultProfile().httpUserAgent())\n+    \"\"\"Make the default WebEngine user agent available via parsed_user_agent.\"\"\"\n+    actual_default_profile = QWebEngineProfile.defaultProfile()\n+    assert actual_default_profile is not None\n+    _init_user_agent_str(actual_default_profile.httpUserAgent())\n \n \n def _init_profile(profile: QWebEngineProfile) -&gt; None:\n@@ -397,7 +396,11 @@ def _init_default_profile():\n     \"\"\"Init the default QWebEngineProfile.\"\"\"\n     global default_profile\n \n-    default_profile = QWebEngineProfile.defaultProfile()\n+    if machinery.IS_QT6:\n+        default_profile = QWebEngineProfile(\"Default\")\n+    else:\n+        default_profile = QWebEngineProfile.defaultProfile()\n+    assert not default_profile.isOffTheRecord()\n \n     assert parsed_user_agent is None  # avoid earlier profile initialization\n     non_ua_version = version.qtwebengine_versions(avoid_init=True)\n@@ -449,12 +452,21 @@ def _init_site_specific_quirks():\n                   \"AppleWebKit/{webkit_version} (KHTML, like Gecko) \"\n                   \"{upstream_browser_key}/{upstream_browser_version} \"\n                   \"Safari/{webkit_version}\")\n-    new_chrome_ua = (\"Mozilla/5.0 ({os_info}) \"\n-                     \"AppleWebKit/537.36 (KHTML, like Gecko) \"\n-                     \"Chrome/99 \"\n-                     \"Safari/537.36\")\n     firefox_ua = \"Mozilla/5.0 ({os_info}; rv:90.0) Gecko/20100101 Firefox/90.0\"\n \n+    def maybe_newer_chrome_ua(at_least_version):\n+        \"\"\"Return a new UA if our current chrome version isn't at least at_least_version.\"\"\"\n+        current_chome_version = version.qtwebengine_versions().chromium_major\n+        if current_chome_version &gt;= at_least_version:\n+            return None\n+\n+        return (\n+            \"Mozilla/5.0 ({os_info}) \"\n+            \"AppleWebKit/537.36 (KHTML, like Gecko) \"\n+            f\"Chrome/{at_least_version} \"\n+            \"Safari/537.36\"\n+        )\n+\n     user_agents = [\n         # Needed to avoid a \"\"WhatsApp works with Google Chrome 36+\" error\n         # page which doesn't allow to use WhatsApp Web at all. Also see the\n@@ -469,12 +481,14 @@ def _init_site_specific_quirks():\n \n         # Needed because Slack adds an error which prevents using it relatively\n         # aggressively, despite things actually working fine.\n-        # September 2020: Qt 5.12 works, but Qt &lt;= 5.11 shows the error.\n-        # https://github.com/qutebrowser/qutebrowser/issues/4669\n-        (\"ua-slack\", 'https://*.slack.com/*', new_chrome_ua),\n+        # October 2023: Slack claims they only support 112+. On #7951 at least\n+        # one user claims it still works fine on 108 based Qt versions.\n+        (\"ua-slack\", 'https://*.slack.com/*', maybe_newer_chrome_ua(112)),\n     ]\n \n     for name, pattern, ua in user_agents:\n+        if not ua:\n+            continue\n         if name not in config.val.content.site_specific_quirks.skip:\n             config.instance.set_obj('content.headers.user_agent', ua,\n                                     pattern=urlmatch.UrlPattern(pattern),\n@@ -489,20 +503,37 @@ def _init_site_specific_quirks():\n         )\n \n \n-def _init_devtools_settings():\n-    \"\"\"Make sure the devtools always get images/JS permissions.\"\"\"\n-    settings: List[Tuple[str, Any]] = [\n+def _init_default_settings():\n+    \"\"\"Set permissions required for internal functionality.\n+\n+    - Make sure the devtools always get images/JS permissions.\n+    - On Qt 6, make sure files in the data path can load external resources.\n+    \"\"\"\n+    devtools_settings: List[Tuple[str, Any]] = [\n         ('content.javascript.enabled', True),\n         ('content.images', True),\n         ('content.cookies.accept', 'all'),\n     ]\n \n-    for setting, value in settings:\n+    for setting, value in devtools_settings:\n         for pattern in ['chrome-devtools://*', 'devtools://*']:\n             config.instance.set_obj(setting, value,\n                                     pattern=urlmatch.UrlPattern(pattern),\n                                     hide_userconfig=True)\n \n+    if machinery.IS_QT6:\n+        userscripts_settings: List[Tuple[str, Any]] = [\n+            (\"content.local_content_can_access_remote_urls\", True),\n+            (\"content.local_content_can_access_file_urls\", False),\n+        ]\n+        # https://codereview.qt-project.org/c/qt/qtwebengine/+/375672\n+        url = pathlib.Path(standarddir.data(), \"userscripts\").as_uri()\n+        for setting, value in userscripts_settings:\n+            config.instance.set_obj(setting,\n+                                    value,\n+                                    pattern=urlmatch.UrlPattern(f\"{url}/*\"),\n+                                    hide_userconfig=True)\n+\n \n def init():\n     \"\"\"Initialize the global QWebSettings.\"\"\"\n@@ -537,13 +568,17 @@ def init():\n     _global_settings = WebEngineSettings(_SettingsWrapper())\n \n     log.init.debug(\"Initializing profiles...\")\n-    _init_default_profile()\n+\n+    # Apply potential resource patches while initializing profiles.\n+    with pakjoy.patch_webengine():\n+        _init_default_profile()\n+\n     init_private_profile()\n     config.instance.changed.connect(_update_settings)\n \n     log.init.debug(\"Misc initialization...\")\n     _init_site_specific_quirks()\n-    _init_devtools_settings()\n+    _init_default_settings()\n \n \n def shutdown():\ndiff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py\nindex fb45a2f46..48ae7ea50 100644\n--- a/qutebrowser/browser/webengine/webenginetab.py\n+++ b/qutebrowser/browser/webengine/webenginetab.py\n@@ -1,35 +1,20 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n-\"\"\"Wrapper over a QWebEngineView.\"\"\"\n+\"\"\"Wrapper over a WebEngineView.\"\"\"\n \n import math\n+import struct\n import functools\n import dataclasses\n import re\n import html as html_utils\n from typing import cast, Union, Optional\n \n-from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QTimer, QUrl,\n-                          QObject)\n+from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl,\n+                          QObject, QByteArray)\n from qutebrowser.qt.network import QAuthenticator\n-from qutebrowser.qt.webenginewidgets import QWebEngineView\n from qutebrowser.qt.webenginecore import QWebEnginePage, QWebEngineScript, QWebEngineHistory\n \n from qutebrowser.config import config\n@@ -39,8 +24,8 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,\n                                            webengineinspector)\n \n from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,\n-                               resources, message, jinja, debug, version)\n-from qutebrowser.qt import sip\n+                               resources, message, jinja, debug, version, urlutils)\n+from qutebrowser.qt import sip, machinery\n from qutebrowser.misc import objects, miscwidgets\n \n \n@@ -58,8 +43,6 @@ class WebEngineAction(browsertab.AbstractAction):\n     \"\"\"QtWebEngine implementations related to web actions.\"\"\"\n \n     _widget: webview.WebEngineView\n-\n-    action_class = QWebEnginePage\n     action_base = QWebEnginePage.WebAction\n \n     def exit_fullscreen(self):\n@@ -83,6 +66,14 @@ class WebEnginePrinting(browsertab.AbstractPrinting):\n \n     _widget: webview.WebEngineView\n \n+    def connect_signals(self):\n+        \"\"\"Called from WebEngineTab.connect_signals.\"\"\"\n+        page = self._widget.page()\n+        page.pdfPrintingFinished.connect(self.pdf_printing_finished)\n+        if machinery.IS_QT6:\n+            self._widget.printFinished.connect(self.printing_finished)\n+            # Qt 5 uses callbacks instead\n+\n     def check_pdf_support(self):\n         pass\n \n@@ -90,11 +81,20 @@ class WebEnginePrinting(browsertab.AbstractPrinting):\n         raise browsertab.WebTabError(\n             \"Print previews are unsupported with QtWebEngine\")\n \n-    def to_pdf(self, filename):\n-        self._widget.page().printToPdf(filename)\n+    def to_pdf(self, path):\n+        self._widget.page().printToPdf(str(path))\n+\n+    def to_printer(self, printer):\n+        if machinery.IS_QT5:\n+            self._widget.page().print(printer, self.printing_finished.emit)\n+        else:  # Qt 6\n+            self._widget.print(printer)\n \n-    def to_printer(self, printer, callback=lambda ok: None):\n-        self._widget.page().print(printer, callback)\n+\n+if machinery.IS_QT5:\n+    _FindFlagType = Union[QWebEnginePage.FindFlag, QWebEnginePage.FindFlags]\n+else:\n+    _FindFlagType = QWebEnginePage.FindFlag\n \n \n @dataclasses.dataclass\n@@ -105,13 +105,11 @@ class _FindFlags:\n \n     def to_qt(self):\n         \"\"\"Convert flags into Qt flags.\"\"\"\n-        # FIXME:mypy Those should be correct, reevaluate with PyQt6-stubs\n-        flags = QWebEnginePage.FindFlag(0)\n+        flags: _FindFlagType = QWebEnginePage.FindFlag(0)\n         if self.case_sensitive:\n-            flags |= (  # type: ignore[assignment]\n-                QWebEnginePage.FindFlag.FindCaseSensitively)\n+            flags |= QWebEnginePage.FindFlag.FindCaseSensitively\n         if self.backward:\n-            flags |= QWebEnginePage.FindFlag.FindBackward  # type: ignore[assignment]\n+            flags |= QWebEnginePage.FindFlag.FindBackward\n         return flags\n \n     def __bool__(self):\n@@ -159,22 +157,6 @@ class WebEngineSearch(browsertab.AbstractSearch):\n \n     def connect_signals(self):\n         \"\"\"Connect the signals necessary for this class to function.\"\"\"\n-        # The API necessary to stop wrapping was added in this version\n-        if not qtutils.version_check(\"5.14\"):\n-            return\n-\n-        try:\n-            # pylint: disable=unused-import\n-            from qutebrowser.qt.webenginecore import QWebEngineFindTextResult\n-        except ImportError:\n-            # WORKAROUND for some odd PyQt/packaging bug where the\n-            # findTextResult signal is available, but QWebEngineFindTextResult\n-            # is not. Seems to happen on e.g. Gentoo.\n-            log.webview.warning(\"Could not import QWebEngineFindTextResult \"\n-                                \"despite running on Qt 5.14. You might need \"\n-                                \"to rebuild PyQtWebEngine.\")\n-            return\n-\n         self._widget.page().findTextFinished.connect(self._on_find_finished)\n \n     def _find(self, text, flags, callback, caller):\n@@ -182,7 +164,7 @@ class WebEngineSearch(browsertab.AbstractSearch):\n         self.search_displayed = True\n         self._pending_searches += 1\n \n-        def wrapped_callback(found):\n+        def wrapped_callback(cb_arg):\n             \"\"\"Wrap the callback to do debug logging.\"\"\"\n             self._pending_searches -= 1\n             if self._pending_searches &gt; 0:\n@@ -200,6 +182,11 @@ class WebEngineSearch(browsertab.AbstractSearch):\n                                   \"widget\")\n                 return\n \n+            # bool in Qt 5, QWebEngineFindTextResult in Qt 6\n+            # Once we drop Qt 5, we might also want to call callbacks with the\n+            # QWebEngineFindTextResult instead of the bool.\n+            found = cb_arg if isinstance(cb_arg, bool) else cb_arg.numberOfMatches() &gt; 0\n+\n             found_text = 'found' if found else \"didn't find\"\n             if flags:\n                 flag_text = f'with flags {flags}'\n@@ -305,6 +292,8 @@ class WebEngineCaret(browsertab.AbstractCaret):\n         flags = set()\n         if utils.is_windows:\n             flags.add('windows')\n+        if 'caret' in objects.debug_flags:\n+            flags.add('debug')\n         return list(flags)\n \n     @pyqtSlot(usertypes.KeyMode)\n@@ -400,6 +389,8 @@ class WebEngineCaret(browsertab.AbstractCaret):\n         # https://bugreports.qt.io/browse/QTBUG-53134\n         # Even on Qt 5.10 selectedText() seems to work poorly, see\n         # https://github.com/qutebrowser/qutebrowser/issues/3523\n+        # With Qt 6.2-6.5, there still seem to be issues (especially with\n+        # multi-line text)\n         self._tab.run_js_async(javascript.assemble('caret', 'getSelection'),\n                                callback)\n \n@@ -623,8 +614,16 @@ class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate):\n         self._tab = tab\n         self._history = cast(QWebEngineHistory, None)\n \n+    def _serialize_data(self, stream_version, count, current_index):\n+        return struct.pack(\"&gt;IIi\", stream_version, count, current_index)\n+\n     def serialize(self):\n-        return qtutils.serialize(self._history)\n+        data = qtutils.serialize(self._history)\n+        # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-117489\n+        if data == self._serialize_data(stream_version=4, count=1, current_index=0):\n+            fixed = self._serialize_data(stream_version=4, count=0, current_index=-1)\n+            return QByteArray(fixed)\n+        return data\n \n     def deserialize(self, data):\n         qtutils.deserialize(data, self._history)\n@@ -650,11 +649,13 @@ class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate):\n         self._tab.load_url(url)\n \n     def load_items(self, items):\n-        webengine_version = version.qtwebengine_versions().webengine\n-        if webengine_version &gt;= utils.VersionNumber(5, 15):\n-            self._load_items_workaround(items)\n-            return\n+        self._load_items_workaround(items)\n+\n+    def _load_items_proper(self, items):\n+        \"\"\"Load session items properly.\n \n+        Currently unused, but should be revived.\n+        \"\"\"\n         if items:\n             self._tab.before_load_started.emit(items[-1].url)\n \n@@ -813,9 +814,9 @@ class WebEngineAudio(browsertab.AbstractAudio):\n         self._overridden = False\n \n         # Implements the intended two-second delay specified at\n-        # https://doc.qt.io/qt-5/qwebenginepage.html#recentlyAudibleChanged\n+        # https://doc.qt.io/archives/qt-5.14/qwebenginepage.html#recentlyAudibleChanged\n         delay_ms = 2000\n-        self._silence_timer = QTimer(self)\n+        self._silence_timer = usertypes.Timer(self)\n         self._silence_timer.setSingleShot(True)\n         self._silence_timer.setInterval(delay_ms)\n \n@@ -825,6 +826,8 @@ class WebEngineAudio(browsertab.AbstractAudio):\n         page.recentlyAudibleChanged.connect(self._delayed_recently_audible_changed)\n         self._tab.url_changed.connect(self._on_url_changed)\n         config.instance.changed.connect(self._on_config_changed)\n+        self._silence_timer.timeout.connect(functools.partial(\n+            self.recently_audible_changed.emit, False))\n \n     # WORKAROUND for recentlyAudibleChanged being emitted without delay from the moment\n     # that audio is dropped.\n@@ -840,20 +843,13 @@ class WebEngineAudio(browsertab.AbstractAudio):\n             # Ignore all subsequent calls while the tab is muted with an active timer\n             if timer.isActive():\n                 return\n-            timer.timeout.connect(\n-                functools.partial(self.recently_audible_changed.emit, recently_audible))\n             timer.start()\n \n     def set_muted(self, muted: bool, override: bool = False) -&gt; None:\n-        was_muted = self.is_muted()\n         self._overridden = override\n         assert self._widget is not None\n         page = self._widget.page()\n         page.setAudioMuted(muted)\n-        if was_muted != muted and qtutils.version_check('5.15'):\n-            # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-85118\n-            # so that the tab title at least updates the muted indicator\n-            self.muted_changed.emit(muted)\n \n     def is_muted(self):\n         page = self._widget.page()\n@@ -881,11 +877,8 @@ class _WebEnginePermissions(QObject):\n \n     _widget: webview.WebEngineView\n \n-    # Using 0 as WORKAROUND for:\n-    # https://www.riverbankcomputing.com/pipermail/pyqt/2019-July/041903.html\n-\n     _options = {\n-        0: 'content.notifications.enabled',\n+        QWebEnginePage.Feature.Notifications: 'content.notifications.enabled',\n         QWebEnginePage.Feature.Geolocation: 'content.geolocation',\n         QWebEnginePage.Feature.MediaAudioCapture: 'content.media.audio_capture',\n         QWebEnginePage.Feature.MediaVideoCapture: 'content.media.video_capture',\n@@ -896,7 +889,7 @@ class _WebEnginePermissions(QObject):\n     }\n \n     _messages = {\n-        0: 'show notifications',\n+        QWebEnginePage.Feature.Notifications: 'show notifications',\n         QWebEnginePage.Feature.Geolocation: 'access your location',\n         QWebEnginePage.Feature.MediaAudioCapture: 'record audio',\n         QWebEnginePage.Feature.MediaVideoCapture: 'record video',\n@@ -952,14 +945,7 @@ class _WebEnginePermissions(QObject):\n         permission_str = debug.qenum_key(QWebEnginePage, feature)\n \n         if not url.isValid():\n-            # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-85116\n-            is_qtbug = (qtutils.version_check('5.15.0',\n-                                              compiled=False,\n-                                              exact=True) and\n-                        self._tab.is_private and\n-                        feature == QWebEnginePage.Feature.Notifications)\n-            logger = log.webview.debug if is_qtbug else log.webview.warning\n-            logger(\"Ignoring feature permission {} for invalid URL {}\".format(\n+            log.webview.warning(\"Ignoring feature permission {} for invalid URL {}\".format(\n                 permission_str, url))\n             deny_permission()\n             return\n@@ -970,18 +956,6 @@ class _WebEnginePermissions(QObject):\n             deny_permission()\n             return\n \n-        if (\n-                feature in [QWebEnginePage.Feature.DesktopVideoCapture,\n-                            QWebEnginePage.Feature.DesktopAudioVideoCapture] and\n-                qtutils.version_check('5.13', compiled=False) and\n-                not qtutils.version_check('5.13.2', compiled=False)\n-        ):\n-            # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-78016\n-            log.webview.warning(\"Ignoring desktop sharing request due to \"\n-                                \"crashes in Qt &lt; 5.13.2\")\n-            deny_permission()\n-            return\n-\n         question = shared.feature_permission(\n             url=url.adjusted(QUrl.UrlFormattingOption.RemovePath),\n             option=self._options[feature], msg=self._messages[feature],\n@@ -1089,12 +1063,10 @@ class _WebEngineScripts(QObject):\n     def _remove_js(self, name):\n         \"\"\"Remove an early QWebEngineScript.\"\"\"\n         scripts = self._widget.page().scripts()\n-        if hasattr(scripts, 'find'):\n-            # Qt 6\n+        if machinery.IS_QT6:\n             for script in scripts.find(f'_qute_{name}'):\n                 scripts.remove(script)\n-        else:\n-            # Qt 5\n+        else:  # Qt 5\n             script = scripts.findScript(f'_qute_{name}')\n             if not script.isNull():\n                 scripts.remove(script)\n@@ -1210,13 +1182,11 @@ class _WebEngineScripts(QObject):\n             log.greasemonkey.debug(f'adding script: {new_script.name()}')\n             page_scripts.insert(new_script)\n \n-    def _inject_site_specific_quirks(self):\n-        \"\"\"Add site-specific quirk scripts.\"\"\"\n-        if not config.val.content.site_specific_quirks.enabled:\n-            return\n-\n+    def _get_quirks(self):\n+        \"\"\"Get a list of all available JS quirks.\"\"\"\n         versions = version.qtwebengine_versions()\n-        quirks = [\n+        return [\n+            # FIXME:qt6 Double check which of those are still required\n             _Quirk(\n                 'whatsapp_web',\n                 injection_point=QWebEngineScript.InjectionPoint.DocumentReady,\n@@ -1228,21 +1198,23 @@ class _WebEngineScripts(QObject):\n                 # will be an UA quirk once we set the JS UA as well\n                 name='ua-googledocs',\n             ),\n+\n             _Quirk(\n                 'string_replaceall',\n                 predicate=versions.webengine &lt; utils.VersionNumber(5, 15, 3),\n             ),\n             _Quirk(\n-                'globalthis',\n-                predicate=versions.webengine &lt; utils.VersionNumber(5, 13),\n+                'array_at',\n+                predicate=versions.webengine &lt; utils.VersionNumber(6, 3),\n             ),\n-            _Quirk(\n-                'object_fromentries',\n-                predicate=versions.webengine &lt; utils.VersionNumber(5, 13),\n-            )\n         ]\n \n-        for quirk in quirks:\n+    def _inject_site_specific_quirks(self):\n+        \"\"\"Add site-specific quirk scripts.\"\"\"\n+        if not config.val.content.site_specific_quirks.enabled:\n+            return\n+\n+        for quirk in self._get_quirks():\n             if not quirk.predicate:\n                 continue\n             src = resources.read_file(f'javascript/quirks/{quirk.filename}.user.js')\n@@ -1296,9 +1268,10 @@ class WebEngineTab(browsertab.AbstractTab):\n \n     abort_questions = pyqtSignal()\n \n-    _widget: QWebEngineView\n+    _widget: webview.WebEngineView\n     search: WebEngineSearch\n     audio: WebEngineAudio\n+    printing: WebEnginePrinting\n \n     def __init__(self, *, win_id, mode_manager, private, parent=None):\n         super().__init__(win_id=win_id,\n@@ -1313,7 +1286,7 @@ class WebEngineTab(browsertab.AbstractTab):\n                                     tab=self, parent=self)\n         self.zoom = WebEngineZoom(tab=self, parent=self)\n         self.search = WebEngineSearch(tab=self, parent=self)\n-        self.printing = WebEnginePrinting(tab=self)\n+        self.printing = WebEnginePrinting(tab=self, parent=self)\n         self.elements = WebEngineElements(tab=self)\n         self.action = WebEngineAction(tab=self)\n         self.audio = WebEngineAudio(tab=self, parent=self)\n@@ -1329,6 +1302,9 @@ class WebEngineTab(browsertab.AbstractTab):\n         self._child_event_filter = None\n         self._saved_zoom = None\n         self._scripts.init()\n+        # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223\n+        self._needs_qtbug65223_workaround = (\n+            version.qtwebengine_versions().webengine &lt; utils.VersionNumber(5, 15, 5))\n \n     def _set_widget(self, widget):\n         # pylint: disable=protected-access\n@@ -1414,13 +1390,9 @@ class WebEngineTab(browsertab.AbstractTab):\n     def title(self):\n         return self._widget.title()\n \n-    def renderer_process_pid(self) -&gt; Optional[int]:\n+    def renderer_process_pid(self) -&gt; int:\n         page = self._widget.page()\n-        try:\n-            return page.renderProcessPid()\n-        except AttributeError:\n-            # Added in Qt 5.15\n-            return None\n+        return page.renderProcessPid()\n \n     def icon(self):\n         return self._widget.icon()\n@@ -1461,8 +1433,7 @@ class WebEngineTab(browsertab.AbstractTab):\n         title = self.title()\n         title_url = QUrl(url)\n         title_url.setScheme('')\n-        title_url_str = title_url.toDisplayString(\n-            QUrl.UrlFormattingOption.RemoveScheme)  # type: ignore[arg-type]\n+        title_url_str = title_url.toDisplayString(urlutils.FormatOption.REMOVE_SCHEME)\n         if title == title_url_str.strip('/'):\n             title = \"\"\n \n@@ -1506,9 +1477,9 @@ class WebEngineTab(browsertab.AbstractTab):\n             log.network.debug(\"Asking for credentials\")\n             answer = shared.authentication_required(\n                 url, authenticator, abort_on=[self.abort_questions])\n-        if not netrc_success and answer is None:\n-            log.network.debug(\"Aborting auth\")\n-            sip.assign(authenticator, QAuthenticator())\n+            if answer is None:\n+                log.network.debug(\"Aborting auth\")\n+                sip.assign(authenticator, QAuthenticator())\n \n     @pyqtSlot()\n     def _on_load_started(self):\n@@ -1570,24 +1541,25 @@ class WebEngineTab(browsertab.AbstractTab):\n \n     @pyqtSlot(int)\n     def _on_load_progress(self, perc: int) -&gt; None:\n-        \"\"\"QtWebEngine-specific loadProgress workarounds.\n-\n-        WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223\n-        \"\"\"\n+        \"\"\"QtWebEngine-specific loadProgress workarounds.\"\"\"\n         super()._on_load_progress(perc)\n-        if (perc == 100 and\n-                self.load_status() != usertypes.LoadStatus.error):\n+        if (\n+            self._needs_qtbug65223_workaround and\n+            perc == 100 and\n+            self.load_status() != usertypes.LoadStatus.error\n+        ):\n             self._update_load_status(ok=True)\n \n     @pyqtSlot(bool)\n     def _on_load_finished(self, ok: bool) -&gt; None:\n-        \"\"\"QtWebEngine-specific loadFinished workarounds.\"\"\"\n+        \"\"\"QtWebEngine-specific loadFinished code.\"\"\"\n         super()._on_load_finished(ok)\n \n-        if not ok:\n-            # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223\n+        if not self._needs_qtbug65223_workaround or not ok:\n+            # With the workaround, this should only run with ok=False\n             self._update_load_status(ok)\n \n+        if not ok:\n             self.dump_async(functools.partial(\n                 self._error_page_workaround,\n                 self.settings.test_attribute('content.javascript.enabled')))\n@@ -1607,7 +1579,7 @@ class WebEngineTab(browsertab.AbstractTab):\n         log.network.debug(\"First party URL: {}\".format(first_party_url))\n \n         if error.is_overridable():\n-            error.ignore = shared.ignore_certificate_error(\n+            shared.handle_certificate_error(\n                 request_url=url,\n                 first_party_url=first_party_url,\n                 error=error,\n@@ -1620,28 +1592,6 @@ class WebEngineTab(browsertab.AbstractTab):\n         log.network.debug(\"ignore {}, URL {}, requested {}\".format(\n             error.ignore, url, self.url(requested=True)))\n \n-        # WORKAROUND for https://codereview.qt-project.org/c/qt/qtwebengine/+/270556\n-        show_non_overr_cert_error = (\n-            not error.is_overridable() and (\n-                # Affected Qt versions:\n-                # 5.13 before 5.13.2\n-                # 5.12 before 5.12.6\n-                # &lt; 5.12 (which is unsupported)\n-                (qtutils.version_check('5.13') and\n-                 not qtutils.version_check('5.13.2')) or\n-                (qtutils.version_check('5.12') and\n-                 not qtutils.version_check('5.12.6'))\n-            )\n-        )\n-\n-        # We can't really know when to show an error page, as the error might\n-        # have happened when loading some resource.\n-        is_resource = (\n-            first_party_url.isValid() and\n-            url.matches(first_party_url, QUrl.UrlFormattingOption.RemoveScheme))\n-        if show_non_overr_cert_error and is_resource:\n-            self._show_error_page(url, str(error))\n-\n     @pyqtSlot()\n     def _on_print_requested(self):\n         \"\"\"Slot for window.print() in JS.\"\"\"\n@@ -1650,27 +1600,30 @@ class WebEngineTab(browsertab.AbstractTab):\n         except browsertab.WebTabError as e:\n             message.error(str(e))\n \n-    @pyqtSlot(QUrl)\n-    def _on_url_changed(self, url: QUrl) -&gt; None:\n-        \"\"\"Update settings for the current URL.\n-\n-        Normally this is done below in _on_navigation_request, but we also need\n-        to do it here as WORKAROUND for\n-        https://bugreports.qt.io/browse/QTBUG-77137\n-\n-        Since update_for_url() is idempotent, it doesn't matter much if we end\n-        up doing it twice.\n-        \"\"\"\n-        super()._on_url_changed(url)\n-        if (url.isValid() and\n-                qtutils.version_check('5.13') and\n-                not qtutils.version_check('5.14')):\n-            self.settings.update_for_url(url)\n-\n     @pyqtSlot(usertypes.NavigationRequest)\n     def _on_navigation_request(self, navigation):\n         super()._on_navigation_request(navigation)\n \n+        local_schemes = {\"qute\", \"file\"}\n+        qtwe_ver = version.qtwebengine_versions().webengine\n+        if (\n+            navigation.accepted and\n+            self.url().scheme().lower() in local_schemes and\n+            navigation.url.scheme().lower() not in local_schemes and\n+            (navigation.navigation_type ==\n+                usertypes.NavigationRequest.Type.link_clicked) and\n+            navigation.is_main_frame and\n+            (utils.VersionNumber(6, 2) &lt;= qtwe_ver &lt; utils.VersionNumber(6, 2, 5) or\n+             utils.VersionNumber(6, 3) &lt;= qtwe_ver &lt; utils.VersionNumber(6, 3, 1))\n+        ):\n+            # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-103778\n+            log.webview.debug(\n+                \"Working around blocked request from local page \"\n+                f\"{self.url().toDisplayString()}\"\n+            )\n+            navigation.accepted = False\n+            self.load_url(navigation.url)\n+\n         if not navigation.accepted or not navigation.is_main_frame:\n             return\n \n@@ -1723,16 +1676,7 @@ class WebEngineTab(browsertab.AbstractTab):\n         page.contentsSizeChanged.connect(self.contents_size_changed)\n         page.navigation_request.connect(self._on_navigation_request)\n         page.printRequested.connect(self._on_print_requested)\n-\n-        try:\n-            # pylint: disable=unused-import\n-            from qutebrowser.qt.webenginewidgets import (\n-                QWebEngineClientCertificateSelection)\n-        except ImportError:\n-            pass\n-        else:\n-            page.selectClientCertificate.connect(\n-                self._on_select_client_certificate)\n+        page.selectClientCertificate.connect(self._on_select_client_certificate)\n \n         view.titleChanged.connect(self.title_changed)\n         view.urlChanged.connect(self._on_url_changed)\n@@ -1743,12 +1687,7 @@ class WebEngineTab(browsertab.AbstractTab):\n         page.loadFinished.connect(self._on_history_trigger)\n         page.loadFinished.connect(self._restore_zoom)\n         page.loadFinished.connect(self._on_load_finished)\n-\n-        try:\n-            page.renderProcessPidChanged.connect(self._on_renderer_process_pid_changed)\n-        except AttributeError:\n-            # Added in Qt 5.15.0\n-            pass\n+        page.renderProcessPidChanged.connect(self._on_renderer_process_pid_changed)\n \n         self.shutting_down.connect(self.abort_questions)\n         self.load_started.connect(self.abort_questions)\n@@ -1756,5 +1695,6 @@ class WebEngineTab(browsertab.AbstractTab):\n         # pylint: disable=protected-access\n         self.audio._connect_signals()\n         self.search.connect_signals()\n+        self.printing.connect_signals()\n         self._permissions.connect_signals()\n         self._scripts.connect_signals()\ndiff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py\nindex 3d94c5206..96c0c97e5 100644\n--- a/qutebrowser/browser/webengine/webview.py\n+++ b/qutebrowser/browser/webengine/webview.py\n@@ -1,35 +1,25 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The main browser widget for QtWebEngine.\"\"\"\n \n-from typing import List, Iterable\n+import mimetypes\n+from typing import List, Iterable, Optional\n \n-from qutebrowser.qt.core import pyqtSignal, QUrl\n+from qutebrowser.qt import machinery\n+from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QUrl\n from qutebrowser.qt.gui import QPalette\n from qutebrowser.qt.webenginewidgets import QWebEngineView\n-from qutebrowser.qt.webenginecore import QWebEnginePage\n+from qutebrowser.qt.webenginecore import (\n+    QWebEnginePage, QWebEngineCertificateError, QWebEngineSettings,\n+    QWebEngineHistory,\n+)\n \n from qutebrowser.browser import shared\n from qutebrowser.browser.webengine import webenginesettings, certificateerror\n from qutebrowser.config import config\n-from qutebrowser.utils import log, debug, usertypes\n+from qutebrowser.utils import log, debug, usertypes, qtutils\n \n \n _QB_FILESELECTION_MODES = {\n@@ -55,7 +45,9 @@ class WebEngineView(QWebEngineView):\n         self._win_id = win_id\n         self._tabdata = tabdata\n \n-        theme_color = self.style().standardPalette().color(QPalette.ColorRole.Base)\n+        style = self.style()\n+        assert style is not None\n+        theme_color = style.standardPalette().color(QPalette.ColorRole.Base)\n         if private:\n             assert webenginesettings.private_profile is not None\n             profile = webenginesettings.private_profile\n@@ -140,6 +132,57 @@ class WebEngineView(QWebEngineView):\n             return\n         super().contextMenuEvent(ev)\n \n+    def page(self) -&gt; \"WebEnginePage\":\n+        \"\"\"Return the page for this view.\"\"\"\n+        maybe_page = super().page()\n+        assert maybe_page is not None\n+        assert isinstance(maybe_page, WebEnginePage)\n+        return maybe_page\n+\n+    def settings(self) -&gt; \"QWebEngineSettings\":\n+        \"\"\"Return the settings for this view.\"\"\"\n+        maybe_settings = super().settings()\n+        assert maybe_settings is not None\n+        return maybe_settings\n+\n+    def history(self) -&gt; \"QWebEngineHistory\":\n+        \"\"\"Return the history for this view.\"\"\"\n+        maybe_history = super().history()\n+        assert maybe_history is not None\n+        return maybe_history\n+\n+\n+def extra_suffixes_workaround(upstream_mimetypes):\n+    \"\"\"Return any extra suffixes for mimetypes in upstream_mimetypes.\n+\n+    Return any file extensions (aka suffixes) for mimetypes listed in\n+    upstream_mimetypes that are not already contained in there.\n+\n+    WORKAROUND: for https://bugreports.qt.io/browse/QTBUG-116905\n+    Affected Qt versions &gt; 6.2.2 (probably) &lt; 6.7.0\n+    \"\"\"\n+    if not (\n+        qtutils.version_check(\"6.2.3\", compiled=False)\n+        and not qtutils.version_check(\"6.7.0\", compiled=False)\n+    ):\n+        return set()\n+\n+    suffixes = {entry for entry in upstream_mimetypes if entry.startswith(\".\")}\n+    mimes = {entry for entry in upstream_mimetypes if \"/\" in entry}\n+    python_suffixes = set()\n+    for mime in mimes:\n+        if mime.endswith(\"/*\"):\n+            python_suffixes.update(\n+                [\n+                    suffix\n+                    for suffix, mimetype in mimetypes.types_map.items()\n+                    if mimetype.startswith(mime[:-1])\n+                ]\n+            )\n+        else:\n+            python_suffixes.update(mimetypes.guess_all_extensions(mime))\n+    return python_suffixes - suffixes\n+\n \n class WebEnginePage(QWebEnginePage):\n \n@@ -151,8 +194,9 @@ class WebEnginePage(QWebEnginePage):\n \n     Signals:\n         certificate_error: Emitted on certificate errors.\n-                           Needs to be directly connected to a slot setting the\n-                           'ignore' attribute.\n+                           Needs to be directly connected to a slot calling\n+                           .accept_certificate(), .reject_certificate, or\n+                           .defer().\n         shutting_down: Emitted when the page is shutting down.\n         navigation_request: Emitted on acceptNavigationRequest.\n     \"\"\"\n@@ -161,12 +205,41 @@ class WebEnginePage(QWebEnginePage):\n     shutting_down = pyqtSignal()\n     navigation_request = pyqtSignal(usertypes.NavigationRequest)\n \n+    _JS_LOG_LEVEL_MAPPING = {\n+        QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel:\n+            usertypes.JsLogLevel.info,\n+        QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel:\n+            usertypes.JsLogLevel.warning,\n+        QWebEnginePage.JavaScriptConsoleMessageLevel.ErrorMessageLevel:\n+            usertypes.JsLogLevel.error,\n+    }\n+\n+    _NAVIGATION_TYPE_MAPPING = {\n+        QWebEnginePage.NavigationType.NavigationTypeLinkClicked:\n+            usertypes.NavigationRequest.Type.link_clicked,\n+        QWebEnginePage.NavigationType.NavigationTypeTyped:\n+            usertypes.NavigationRequest.Type.typed,\n+        QWebEnginePage.NavigationType.NavigationTypeFormSubmitted:\n+            usertypes.NavigationRequest.Type.form_submitted,\n+        QWebEnginePage.NavigationType.NavigationTypeBackForward:\n+            usertypes.NavigationRequest.Type.back_forward,\n+        QWebEnginePage.NavigationType.NavigationTypeReload:\n+            usertypes.NavigationRequest.Type.reload,\n+        QWebEnginePage.NavigationType.NavigationTypeOther:\n+            usertypes.NavigationRequest.Type.other,\n+        QWebEnginePage.NavigationType.NavigationTypeRedirect:\n+            usertypes.NavigationRequest.Type.redirect,\n+    }\n+\n     def __init__(self, *, theme_color, profile, parent=None):\n         super().__init__(profile, parent)\n         self._is_shutting_down = False\n         self._theme_color = theme_color\n         self._set_bg_color()\n         config.instance.changed.connect(self._set_bg_color)\n+        if machinery.IS_QT6:\n+            self.certificateError.connect(self._handle_certificate_error)\n+            # Qt 5: Overridden method instead of signal\n \n     @config.change_filter('colors.webpage.bg')\n     def _set_bg_color(self):\n@@ -179,11 +252,17 @@ class WebEnginePage(QWebEnginePage):\n         self._is_shutting_down = True\n         self.shutting_down.emit()\n \n-    def certificateError(self, error):\n+    @pyqtSlot(QWebEngineCertificateError)\n+    def _handle_certificate_error(self, qt_error):\n         \"\"\"Handle certificate errors coming from Qt.\"\"\"\n-        error = certificateerror.CertificateErrorWrapper(error)\n+        error = certificateerror.CertificateErrorWrapper(qt_error)\n         self.certificate_error.emit(error)\n-        return error.ignore\n+        # Right now, we never defer accepting, due to a PyQt bug\n+        return error.certificate_was_accepted()\n+\n+    if machinery.IS_QT5:\n+        # Overridden method instead of signal\n+        certificateError = _handle_certificate_error  # noqa: N815\n \n     def javaScriptConfirm(self, url, js_msg):\n         \"\"\"Override javaScriptConfirm to use qutebrowser prompts.\"\"\"\n@@ -217,42 +296,16 @@ class WebEnginePage(QWebEnginePage):\n \n     def javaScriptConsoleMessage(self, level, msg, line, source):\n         \"\"\"Log javascript messages to qutebrowser's log.\"\"\"\n-        level_map = {\n-            QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel: usertypes.JsLogLevel.info,\n-            QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel: usertypes.JsLogLevel.warning,\n-            QWebEnginePage.JavaScriptConsoleMessageLevel.ErrorMessageLevel: usertypes.JsLogLevel.error,\n-        }\n-        shared.javascript_log_message(level_map[level], source, line, msg)\n+        shared.javascript_log_message(self._JS_LOG_LEVEL_MAPPING[level], source, line, msg)\n \n     def acceptNavigationRequest(self,\n                                 url: QUrl,\n                                 typ: QWebEnginePage.NavigationType,\n                                 is_main_frame: bool) -&gt; bool:\n         \"\"\"Override acceptNavigationRequest to forward it to the tab API.\"\"\"\n-        type_map = {\n-            QWebEnginePage.NavigationType.NavigationTypeLinkClicked:\n-                usertypes.NavigationRequest.Type.link_clicked,\n-            QWebEnginePage.NavigationType.NavigationTypeTyped:\n-                usertypes.NavigationRequest.Type.typed,\n-            QWebEnginePage.NavigationType.NavigationTypeFormSubmitted:\n-                usertypes.NavigationRequest.Type.form_submitted,\n-            QWebEnginePage.NavigationType.NavigationTypeBackForward:\n-                usertypes.NavigationRequest.Type.back_forward,\n-            QWebEnginePage.NavigationType.NavigationTypeReload:\n-                usertypes.NavigationRequest.Type.reloaded,\n-            QWebEnginePage.NavigationType.NavigationTypeOther:\n-                usertypes.NavigationRequest.Type.other,\n-        }\n-        try:\n-            type_map[QWebEnginePage.NavigationType.NavigationTypeRedirect] = (\n-                usertypes.NavigationRequest.Type.redirect)\n-        except AttributeError:\n-            # Added in Qt 5.14\n-            pass\n-\n         navigation = usertypes.NavigationRequest(\n             url=url,\n-            navigation_type=type_map.get(\n+            navigation_type=self._NAVIGATION_TYPE_MAPPING.get(\n                 typ, usertypes.NavigationRequest.Type.other),\n             is_main_frame=is_main_frame)\n         self.navigation_request.emit(navigation)\n@@ -261,13 +314,28 @@ class WebEnginePage(QWebEnginePage):\n     def chooseFiles(\n         self,\n         mode: QWebEnginePage.FileSelectionMode,\n-        old_files: Iterable[str],\n-        accepted_mimetypes: Iterable[str],\n+        old_files: Iterable[Optional[str]],\n+        accepted_mimetypes: Iterable[Optional[str]],\n     ) -&gt; List[str]:\n         \"\"\"Override chooseFiles to (optionally) invoke custom file uploader.\"\"\"\n+        accepted_mimetypes_filtered = [m for m in accepted_mimetypes if m is not None]\n+        old_files_filtered = [f for f in old_files if f is not None]\n+        extra_suffixes = extra_suffixes_workaround(accepted_mimetypes_filtered)\n+        if extra_suffixes:\n+            log.webview.debug(\n+                \"adding extra suffixes to filepicker: \"\n+                f\"before={accepted_mimetypes_filtered} \"\n+                f\"added={extra_suffixes}\",\n+            )\n+            accepted_mimetypes_filtered = list(\n+                accepted_mimetypes_filtered\n+            ) + list(extra_suffixes)\n+\n         handler = config.val.fileselect.handler\n         if handler == \"default\":\n-            return super().chooseFiles(mode, old_files, accepted_mimetypes)\n+            return super().chooseFiles(\n+                mode, old_files_filtered, accepted_mimetypes_filtered,\n+            )\n         assert handler == \"external\", handler\n         try:\n             qb_mode = _QB_FILESELECTION_MODES[mode]\n@@ -275,6 +343,8 @@ class WebEnginePage(QWebEnginePage):\n             log.webview.warning(\n                 f\"Got file selection mode {mode}, but we don't support that!\"\n             )\n-            return super().chooseFiles(mode, old_files, accepted_mimetypes)\n+            return super().chooseFiles(\n+                mode, old_files_filtered, accepted_mimetypes_filtered,\n+            )\n \n         return shared.choose_file(qb_mode=qb_mode)\ndiff --git a/qutebrowser/browser/webkit/__init__.py b/qutebrowser/browser/webkit/__init__.py\nindex 27492235a..05274d2ff 100644\n--- a/qutebrowser/browser/webkit/__init__.py\n+++ b/qutebrowser/browser/webkit/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Classes related to the browser widgets for QtWebKit.\"\"\"\ndiff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py\nindex 2b5b07c46..7654ea83b 100644\n--- a/qutebrowser/browser/webkit/cache.py\n+++ b/qutebrowser/browser/webkit/cache.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"HTTP network cache.\"\"\"\n \ndiff --git a/qutebrowser/browser/webkit/certificateerror.py b/qutebrowser/browser/webkit/certificateerror.py\nindex d58159430..59d9cc897 100644\n--- a/qutebrowser/browser/webkit/certificateerror.py\n+++ b/qutebrowser/browser/webkit/certificateerror.py\n@@ -1,37 +1,28 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"A wrapper over a list of QSslErrors.\"\"\"\n \n-from typing import Sequence\n+from typing import Sequence, Optional\n \n-from qutebrowser.qt.network import QSslError\n+from qutebrowser.qt.network import QSslError, QNetworkReply\n \n-from qutebrowser.utils import usertypes, utils, debug, jinja\n+from qutebrowser.utils import usertypes, utils, debug, jinja, urlutils\n \n \n class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):\n \n     \"\"\"A wrapper over a list of QSslErrors.\"\"\"\n \n-    def __init__(self, errors: Sequence[QSslError]) -&gt; None:\n+    def __init__(self, reply: QNetworkReply, errors: Sequence[QSslError]) -&gt; None:\n+        super().__init__()\n+        self._reply = reply\n         self._errors = tuple(errors)  # needs to be hashable\n+        try:\n+            self._host_tpl: Optional[urlutils.HostTupleType] = urlutils.host_tuple(reply.url())\n+        except ValueError:\n+            self._host_tpl = None\n \n     def __str__(self) -&gt; str:\n         return '\\n'.join(err.errorString() for err in self._errors)\n@@ -43,16 +34,25 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):\n             string=str(self))\n \n     def __hash__(self) -&gt; int:\n-        return hash(self._errors)\n+        return hash((self._host_tpl, self._errors))\n \n     def __eq__(self, other: object) -&gt; bool:\n         if not isinstance(other, CertificateErrorWrapper):\n             return NotImplemented\n-        return self._errors == other._errors\n+        return self._errors == other._errors and self._host_tpl == other._host_tpl\n \n     def is_overridable(self) -&gt; bool:\n         return True\n \n+    def defer(self) -&gt; None:\n+        raise usertypes.UndeferrableError(\"Never deferrable\")\n+\n+    def accept_certificate(self) -&gt; None:\n+        super().accept_certificate()\n+        self._reply.ignoreSslErrors()\n+\n+    # Not overriding reject_certificate because that's default in QNetworkReply\n+\n     def html(self):\n         if len(self._errors) == 1:\n             return super().html()\ndiff --git a/qutebrowser/browser/webkit/cookies.py b/qutebrowser/browser/webkit/cookies.py\nindex fed819cee..9e6ae2f1b 100644\n--- a/qutebrowser/browser/webkit/cookies.py\n+++ b/qutebrowser/browser/webkit/cookies.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Handling of HTTP cookies.\"\"\"\n \ndiff --git a/qutebrowser/browser/webkit/http.py b/qutebrowser/browser/webkit/httpheaders.py\nsimilarity index 87%\nrename from qutebrowser/browser/webkit/http.py\nrename to qutebrowser/browser/webkit/httpheaders.py\nindex 104abf2d3..95b7b7104 100644\n--- a/qutebrowser/browser/webkit/http.py\n+++ b/qutebrowser/browser/webkit/httpheaders.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Parsing functions for various HTTP headers.\"\"\"\n \n@@ -102,7 +87,7 @@ class ContentDisposition:\n \n         if parsed.defects:\n             defects = list(parsed.defects)\n-            if defects != [cls._IGNORED_DEFECT]:  # type: ignore[comparison-overlap]\n+            if defects != [cls._IGNORED_DEFECT]:\n                 raise ContentDispositionError(defects)\n \n         # https://github.com/python/mypy/issues/12314\ndiff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py\nindex 56ad4fb4f..692689b0a 100644\n--- a/qutebrowser/browser/webkit/mhtml.py\n+++ b/qutebrowser/browser/webkit/mhtml.py\n@@ -1,22 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-# Copyright 2015-2018 Daniel Schadt\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Daniel Schadt\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utils for writing an MHTML file.\"\"\"\n \ndiff --git a/qutebrowser/browser/webkit/network/__init__.py b/qutebrowser/browser/webkit/network/__init__.py\nindex c3d713ac2..92c677c29 100644\n--- a/qutebrowser/browser/webkit/network/__init__.py\n+++ b/qutebrowser/browser/webkit/network/__init__.py\n@@ -1,3 +1 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n \"\"\"Modules related to network operations.\"\"\"\ndiff --git a/qutebrowser/browser/webkit/network/filescheme.py b/qutebrowser/browser/webkit/network/filescheme.py\nindex 78c511cf4..61f361623 100644\n--- a/qutebrowser/browser/webkit/network/filescheme.py\n+++ b/qutebrowser/browser/webkit/network/filescheme.py\n@@ -1,25 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-# Copyright 2015-2018 Antoni Boucher (antoyo) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Antoni Boucher (antoyo) \n #\n-# pylint complains when using .render() on jinja templates, so we make it shut\n-# up for this whole module.\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Handler functions for file:... pages.\"\"\"\n \ndiff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py\nindex 36918c36b..06402a547 100644\n--- a/qutebrowser/browser/webkit/network/networkmanager.py\n+++ b/qutebrowser/browser/webkit/network/networkmanager.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Our own QNetworkAccessManager.\"\"\"\n \n@@ -30,7 +15,7 @@ from qutebrowser.qt.network import (QNetworkAccessManager, QNetworkReply, QSslCo\n \n from qutebrowser.config import config\n from qutebrowser.utils import (message, log, usertypes, utils, objreg,\n-                               urlutils, debug)\n+                               urlutils, debug, qtlog)\n from qutebrowser.browser import shared\n from qutebrowser.browser.network import proxy as proxymod\n from qutebrowser.extensions import interceptors\n@@ -103,8 +88,8 @@ def _is_secure_cipher(cipher):\n \n def init():\n     \"\"\"Disable insecure SSL ciphers on old Qt versions.\"\"\"\n-    sslconfig = QSslConfiguration.defaultConfiguration()\n-    default_ciphers = sslconfig.ciphers()\n+    ssl_config = QSslConfiguration.defaultConfiguration()\n+    default_ciphers = ssl_config.ciphers()\n     log.init.vdebug(  # type: ignore[attr-defined]\n         \"Default Qt ciphers: {}\".format(\n             ', '.join(c.name() for c in default_ciphers)))\n@@ -120,7 +105,7 @@ def init():\n     if bad_ciphers:\n         log.init.debug(\"Disabling bad ciphers: {}\".format(\n             ', '.join(c.name() for c in bad_ciphers)))\n-        sslconfig.setCiphers(good_ciphers)\n+        ssl_config.setCiphers(good_ciphers)\n \n \n _SavedErrorsType = MutableMapping[\n@@ -158,7 +143,7 @@ class NetworkManager(QNetworkAccessManager):\n \n     def __init__(self, *, win_id, tab_id, private, parent=None):\n         log.init.debug(\"Initializing NetworkManager\")\n-        with log.disable_qt_msghandler():\n+        with qtlog.disable_qt_msghandler():\n             # WORKAROUND for a hang when a message is printed - See:\n             # https://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html\n             #\n@@ -239,12 +224,16 @@ class NetworkManager(QNetworkAccessManager):\n \n     def shutdown(self):\n         \"\"\"Abort all running requests.\"\"\"\n-        self.setNetworkAccessible(QNetworkAccessManager.NetworkAccessibility.NotAccessible)\n+        try:\n+            self.setNetworkAccessible(QNetworkAccessManager.NetworkAccessibility.NotAccessible)\n+        except AttributeError:\n+            # Qt 5 only, deprecated seemingly without replacement.\n+            pass\n         self.shutting_down.emit()\n \n     # No @pyqtSlot here, see\n     # https://github.com/qutebrowser/qutebrowser/issues/2213\n-    def on_ssl_errors(self, reply, qt_errors):  # noqa: C901 pragma: no mccabe\n+    def on_ssl_errors(self, reply, qt_errors):\n         \"\"\"Decide if SSL errors should be ignored or not.\n \n         This slot is called on SSL/TLS errors by the self.sslErrors signal.\n@@ -253,7 +242,7 @@ class NetworkManager(QNetworkAccessManager):\n             reply: The QNetworkReply that is encountering the errors.\n             qt_errors: A list of errors.\n         \"\"\"\n-        errors = certificateerror.CertificateErrorWrapper(qt_errors)\n+        errors = certificateerror.CertificateErrorWrapper(reply, qt_errors)\n         log.network.debug(\"Certificate errors: {!r}\".format(errors))\n         try:\n             host_tpl: Optional[urlutils.HostTupleType] = urlutils.host_tuple(\n@@ -281,14 +270,14 @@ class NetworkManager(QNetworkAccessManager):\n         tab = self._get_tab()\n         first_party_url = QUrl() if tab is None else tab.data.last_navigation.url\n \n-        ignore = shared.ignore_certificate_error(\n+        shared.handle_certificate_error(\n             request_url=reply.url(),\n             first_party_url=first_party_url,\n             error=errors,\n             abort_on=abort_on,\n         )\n-        if ignore:\n-            reply.ignoreSslErrors()\n+\n+        if errors.certificate_was_accepted():\n             if host_tpl is not None:\n                 self._accepted_ssl_errors[host_tpl].add(errors)\n         elif host_tpl is not None:\ndiff --git a/qutebrowser/browser/webkit/network/networkreply.py b/qutebrowser/browser/webkit/network/networkreply.py\nindex 6f7abeaab..0ccaabd0e 100644\n--- a/qutebrowser/browser/webkit/network/networkreply.py\n+++ b/qutebrowser/browser/webkit/network/networkreply.py\n@@ -1,28 +1,9 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n #\n # Based on the Eric5 helpviewer,\n # Copyright (c) 2009 - 2014 Detlev Offenbach \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-#\n-# For some reason, a segfault will be triggered if the unnecessary lambdas in\n-# this file aren't there.\n-# pylint: disable=unnecessary-lambda\n \n \"\"\"Special network replies..\"\"\"\n \n@@ -116,7 +97,8 @@ class ErrorNetworkReply(QNetworkReply):\n         # the device to avoid getting a warning.\n         self.setOpenMode(QIODevice.OpenModeFlag.ReadOnly)\n         self.setError(error, errorstring)\n-        QTimer.singleShot(0, lambda: self.error.emit(error))\n+        QTimer.singleShot(0, lambda: self.errorOccurred.emit(error))\n+        # pylint: disable-next=unnecessary-lambda\n         QTimer.singleShot(0, lambda: self.finished.emit())\n \n     def abort(self):\n@@ -128,7 +110,7 @@ class ErrorNetworkReply(QNetworkReply):\n \n     def readData(self, _maxlen):\n         \"\"\"No data available.\"\"\"\n-        return bytes()\n+        return b''\n \n     def isFinished(self):\n         return True\n@@ -144,10 +126,11 @@ class RedirectNetworkReply(QNetworkReply):\n     def __init__(self, new_url, parent=None):\n         super().__init__(parent)\n         self.setAttribute(QNetworkRequest.Attribute.RedirectionTargetAttribute, new_url)\n+        # pylint: disable-next=unnecessary-lambda\n         QTimer.singleShot(0, lambda: self.finished.emit())\n \n     def abort(self):\n         \"\"\"Called when there's e.g. a redirection limit.\"\"\"\n \n     def readData(self, _maxlen):\n-        return bytes()\n+        return b''\ndiff --git a/qutebrowser/browser/webkit/network/webkitqutescheme.py b/qutebrowser/browser/webkit/network/webkitqutescheme.py\nindex 0c4da1a84..f461ed930 100644\n--- a/qutebrowser/browser/webkit/network/webkitqutescheme.py\n+++ b/qutebrowser/browser/webkit/network/webkitqutescheme.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"QtWebKit specific qute://* handlers and glue code.\"\"\"\n \ndiff --git a/qutebrowser/browser/webkit/tabhistory.py b/qutebrowser/browser/webkit/tabhistory.py\nindex 183ffc7a9..80a572385 100644\n--- a/qutebrowser/browser/webkit/tabhistory.py\n+++ b/qutebrowser/browser/webkit/tabhistory.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utilities related to QWebHistory.\"\"\"\n \ndiff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py\nindex 62236dd7b..0400358af 100644\n--- a/qutebrowser/browser/webkit/webkitelem.py\n+++ b/qutebrowser/browser/webkit/webkitelem.py\n@@ -1,29 +1,16 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"QtWebKit specific part of the web element API.\"\"\"\n \n from typing import cast, TYPE_CHECKING, Iterator, List, Optional, Set\n \n from qutebrowser.qt.core import QRect, Qt\n+# pylint: disable=no-name-in-module\n from qutebrowser.qt.webkit import QWebElement, QWebSettings\n from qutebrowser.qt.webkitwidgets import QWebFrame\n+# pylint: enable=no-name-in-module\n \n from qutebrowser.config import config\n from qutebrowser.utils import log, utils, javascript, usertypes\ndiff --git a/qutebrowser/browser/webkit/webkithistory.py b/qutebrowser/browser/webkit/webkithistory.py\nindex 83178a45a..1a4b57fab 100644\n--- a/qutebrowser/browser/webkit/webkithistory.py\n+++ b/qutebrowser/browser/webkit/webkithistory.py\n@@ -1,27 +1,14 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"QtWebKit specific part of history.\"\"\"\n \n import functools\n \n+# pylint: disable=no-name-in-module\n from qutebrowser.qt.webkit import QWebHistoryInterface\n+# pylint: enable=no-name-in-module\n \n from qutebrowser.utils import debug\n from qutebrowser.misc import debugcachestats\ndiff --git a/qutebrowser/browser/webkit/webkitinspector.py b/qutebrowser/browser/webkit/webkitinspector.py\nindex ceec6187c..69cdd1853 100644\n--- a/qutebrowser/browser/webkit/webkitinspector.py\n+++ b/qutebrowser/browser/webkit/webkitinspector.py\n@@ -1,26 +1,13 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Customized QWebInspector for QtWebKit.\"\"\"\n \n+# pylint: disable=no-name-in-module\n from qutebrowser.qt.webkit import QWebSettings\n from qutebrowser.qt.webkitwidgets import QWebInspector, QWebPage\n+# pylint: enable=no-name-in-module\n from qutebrowser.qt.widgets import QWidget\n \n from qutebrowser.browser import inspector\ndiff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py\nindex 2a5c3f765..0ce3d0bf7 100644\n--- a/qutebrowser/browser/webkit/webkitsettings.py\n+++ b/qutebrowser/browser/webkit/webkitsettings.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Bridge from QWebSettings to our own settings.\n \n@@ -29,8 +14,10 @@ import os.path\n \n from qutebrowser.qt.core import QUrl\n from qutebrowser.qt.gui import QFont\n+# pylint: disable=no-name-in-module\n from qutebrowser.qt.webkit import QWebSettings\n from qutebrowser.qt.webkitwidgets import QWebPage\n+# pylint: enable=no-name-in-module\n \n from qutebrowser.config import config, websettings\n from qutebrowser.config.websettings import AttributeInfo as Attr\ndiff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py\nindex 334edeb56..1ae976bea 100644\n--- a/qutebrowser/browser/webkit/webkittab.py\n+++ b/qutebrowser/browser/webkit/webkittab.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Wrapper over our (QtWebKit) WebView.\"\"\"\n \n@@ -27,8 +12,10 @@ from typing import cast, Iterable, Optional\n from qutebrowser.qt.core import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize\n from qutebrowser.qt.gui import QIcon\n from qutebrowser.qt.widgets import QWidget\n+# pylint: disable=no-name-in-module\n from qutebrowser.qt.webkitwidgets import QWebPage, QWebFrame\n from qutebrowser.qt.webkit import QWebSettings, QWebHistory, QWebElement\n+# pylint: enable=no-name-in-module\n from qutebrowser.qt.printsupport import QPrinter\n \n from qutebrowser.browser import browsertab, shared\n@@ -44,7 +31,6 @@ class WebKitAction(browsertab.AbstractAction):\n \n     \"\"\"QtWebKit implementations related to web actions.\"\"\"\n \n-    action_class = QWebPage\n     action_base = QWebPage.WebAction\n \n     _widget: webview.WebView\n@@ -90,16 +76,17 @@ class WebKitPrinting(browsertab.AbstractPrinting):\n     def check_preview_support(self):\n         pass\n \n-    def to_pdf(self, filename):\n+    def to_pdf(self, path):\n         printer = QPrinter()\n-        printer.setOutputFileName(filename)\n-        self.to_printer(printer)\n+        printer.setOutputFileName(str(path))\n+        self._widget.print(printer)\n+        # Can't find out whether there was an error...\n+        self.pdf_printing_finished.emit(str(path), True)\n \n-    def to_printer(self, printer, callback=None):\n+    def to_printer(self, printer):\n         self._widget.print(printer)\n         # Can't find out whether there was an error...\n-        if callback is not None:\n-            callback(True)\n+        self.printing_finished.emit(True)\n \n \n class WebKitSearch(browsertab.AbstractSearch):\n@@ -882,7 +869,7 @@ class WebKitTab(browsertab.AbstractTab):\n                                  tab=self, parent=self)\n         self.zoom = WebKitZoom(tab=self, parent=self)\n         self.search = WebKitSearch(tab=self, parent=self)\n-        self.printing = WebKitPrinting(tab=self)\n+        self.printing = WebKitPrinting(tab=self, parent=self)\n         self.elements = WebKitElements(tab=self)\n         self.action = WebKitAction(tab=self)\n         self.audio = WebKitAudio(tab=self, parent=self)\ndiff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py\nindex ce83ddcc6..595432dc9 100644\n--- a/qutebrowser/browser/webkit/webpage.py\n+++ b/qutebrowser/browser/webkit/webpage.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The main browser widgets.\"\"\"\n \n@@ -27,11 +12,13 @@ from qutebrowser.qt.gui import QDesktopServices\n from qutebrowser.qt.network import QNetworkReply, QNetworkRequest\n from qutebrowser.qt.widgets import QFileDialog\n from qutebrowser.qt.printsupport import QPrintDialog\n+# pylint: disable=no-name-in-module\n from qutebrowser.qt.webkitwidgets import QWebPage, QWebFrame\n+# pylint: enable=no-name-in-module\n \n from qutebrowser.config import websettings, config\n from qutebrowser.browser import pdfjs, shared, downloads, greasemonkey\n-from qutebrowser.browser.webkit import http\n+from qutebrowser.browser.webkit import httpheaders\n from qutebrowser.browser.webkit.network import networkmanager\n from qutebrowser.utils import message, usertypes, log, jinja, objreg\n from qutebrowser.qt import sip\n@@ -276,14 +263,14 @@ class BrowserPage(QWebPage):\n         At some point we might want to implement the MIME Sniffing standard\n         here: https://mimesniff.spec.whatwg.org/\n         \"\"\"\n-        inline, suggested_filename = http.parse_content_disposition(reply)\n+        inline, suggested_filename = httpheaders.parse_content_disposition(reply)\n         download_manager = objreg.get('qtnetwork-download-manager')\n         if not inline:\n             # Content-Disposition: attachment -&gt; force download\n             download_manager.fetch(reply,\n                                    suggested_filename=suggested_filename)\n             return\n-        mimetype, _rest = http.parse_content_type(reply)\n+        mimetype, _rest = httpheaders.parse_content_type(reply)\n         if mimetype == 'image/jpg':\n             # Some servers (e.g. the LinkedIn CDN) send a non-standard\n             # image/jpg (instead of image/jpeg, defined in RFC 1341 section\n@@ -516,7 +503,7 @@ class BrowserPage(QWebPage):\n             QWebPage.NavigationType.NavigationTypeBackOrForward:\n                 usertypes.NavigationRequest.Type.back_forward,\n             QWebPage.NavigationType.NavigationTypeReload:\n-                usertypes.NavigationRequest.Type.reloaded,\n+                usertypes.NavigationRequest.Type.reload,\n             QWebPage.NavigationType.NavigationTypeOther:\n                 usertypes.NavigationRequest.Type.other,\n         }\n@@ -525,7 +512,7 @@ class BrowserPage(QWebPage):\n                                                  navigation_type=type_map[typ],\n                                                  is_main_frame=is_main_frame)\n \n-        if navigation.navigation_type == navigation.Type.reloaded:\n+        if navigation.navigation_type == navigation.Type.reload:\n             self.reloading.emit(navigation.url)\n \n         self.navigation_request.emit(navigation)\ndiff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py\nindex 32028400e..688b70fae 100644\n--- a/qutebrowser/browser/webkit/webview.py\n+++ b/qutebrowser/browser/webkit/webview.py\n@@ -1,31 +1,18 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The main browser widgets.\"\"\"\n \n-from qutebrowser.qt.core import pyqtSignal, Qt, QUrl\n+from qutebrowser.qt.core import pyqtSignal, Qt\n+# pylint: disable=no-name-in-module\n from qutebrowser.qt.webkit import QWebSettings\n from qutebrowser.qt.webkitwidgets import QWebView, QWebPage\n+# pylint: enable=no-name-in-module\n \n from qutebrowser.config import config, stylesheet\n from qutebrowser.keyinput import modeman\n-from qutebrowser.utils import log, usertypes, utils, objreg, debug\n+from qutebrowser.utils import log, usertypes, utils, objreg, debug, urlutils\n from qutebrowser.browser.webkit import webpage\n \n \n@@ -82,8 +69,7 @@ class WebView(QWebView):\n         stylesheet.set_register(self)\n \n     def __repr__(self):\n-        flags = QUrl.ComponentFormattingOption.EncodeUnicode\n-        urlstr = self.url().toDisplayString(flags)  # type: ignore[arg-type]\n+        urlstr = self.url().toDisplayString(urlutils.FormatOption.ENCODE_UNICODE)\n         url = utils.elide(urlstr, 100)\n         return utils.get_repr(self, tab_id=self._tab_id, url=url)\n \ndiff --git a/qutebrowser/commands/__init__.py b/qutebrowser/commands/__init__.py\nindex 2d196ab49..7c6249371 100644\n--- a/qutebrowser/commands/__init__.py\n+++ b/qutebrowser/commands/__init__.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"In qutebrowser, all keybindings are mapped to commands.\n \ndiff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py\nindex febfa7812..0d4bd7ca7 100644\n--- a/qutebrowser/commands/argparser.py\n+++ b/qutebrowser/commands/argparser.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"argparse.ArgumentParser subclass to parse qutebrowser commands.\"\"\"\n \ndiff --git a/qutebrowser/commands/cmdexc.py b/qutebrowser/commands/cmdexc.py\nindex 50488f1e5..4335a10e6 100644\n--- a/qutebrowser/commands/cmdexc.py\n+++ b/qutebrowser/commands/cmdexc.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Exception classes for commands modules.\n \ndiff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py\nindex eee5b7cde..effdcc9b0 100644\n--- a/qutebrowser/commands/command.py\n+++ b/qutebrowser/commands/command.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Contains the Command class, a skeleton for a command.\"\"\"\n \n@@ -77,10 +62,12 @@ class Command:\n     COUNT_COMMAND_VALUES = [usertypes.CommandValue.count,\n                             usertypes.CommandValue.count_tab]\n \n-    def __init__(self, *, handler, name, instance=None, maxsplit=None,\n-                 modes=None, not_modes=None, debug=False, deprecated=False,\n-                 no_cmd_split=False, star_args_optional=False, scope='global',\n-                 backend=None, no_replace_variables=False):\n+    def __init__(\n+        self, *, handler, name, instance=None, maxsplit=None,\n+        modes=None, not_modes=None, debug=False, deprecated=False,\n+        no_cmd_split=False, star_args_optional=False, scope='global',\n+        backend=None, no_replace_variables=False,\n+    ):  # pylint: disable=too-many-arguments\n         if modes is not None and not_modes is not None:\n             raise ValueError(\"Only modes or not_modes can be given!\")\n         if modes is not None:\n@@ -403,21 +390,12 @@ class Command:\n             raise TypeError(\"{}: Legacy tuple type annotation!\".format(\n                 self.name))\n \n-        try:\n-            origin = typing.get_origin(typ)  # type: ignore[attr-defined]\n-        except AttributeError:\n-            # typing.get_origin was added in Python 3.8\n-            origin = getattr(typ, '__origin__', None)\n-\n+        origin = typing.get_origin(typ)\n         if origin is Union:\n-            try:\n-                types = list(typing.get_args(typ))  # type: ignore[attr-defined]\n-            except AttributeError:\n-                # typing.get_args was added in Python 3.8\n-                types = list(typ.__args__)\n-\n+            types = list(typing.get_args(typ))\n             if param.default is not inspect.Parameter.empty:\n                 types.append(type(param.default))\n+\n             choices = self.get_arg_info(param).choices\n             value = argparser.multitype_conv(param, types, value,\n                                              str_choices=choices)\ndiff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py\nindex 5ef46f5e5..d45a18aea 100644\n--- a/qutebrowser/commands/parser.py\n+++ b/qutebrowser/commands/parser.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2021 Ryan Roden-Corrent (rcorre) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Module for parsing commands entered into the browser.\"\"\"\n \ndiff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py\nindex 7cf6ab6fa..0d63d0021 100644\n--- a/qutebrowser/commands/runners.py\n+++ b/qutebrowser/commands/runners.py\n@@ -1,28 +1,13 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Module containing command managers (SearchRunner and CommandRunner).\"\"\"\n \n import traceback\n import re\n import contextlib\n-from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping\n+from typing import TYPE_CHECKING, Callable, Dict, Tuple, Iterator, Mapping, MutableMapping\n \n from qutebrowser.qt.core import pyqtSlot, QUrl, QObject\n \n@@ -36,7 +21,7 @@ if TYPE_CHECKING:\n _ReplacementFunction = Callable[['tabbedbrowser.TabbedBrowser'], str]\n \n \n-last_command = {}\n+last_command: Dict[usertypes.KeyMode, Tuple[str, int]] = {}\n \n \n def _url(tabbed_browser):\n@@ -187,10 +172,10 @@ class CommandRunner(AbstractCommandRunner):\n \n                 result.cmd.run(self._win_id, args, count=count)\n \n-            if result.cmdline[0] == 'repeat-command':\n+            if result.cmdline[0] in ['repeat-command', 'cmd-repeat-last']:\n                 record_last_command = False\n \n-            if result.cmdline[0] in ['macro-record', 'macro-run', 'set-cmd-text']:\n+            if result.cmdline[0] in ['macro-record', 'macro-run', 'set-cmd-text', 'cmd-set-text']:\n                 record_macro = False\n \n         if record_last_command:\ndiff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py\nindex 17164a23a..01710a63c 100644\n--- a/qutebrowser/commands/userscripts.py\n+++ b/qutebrowser/commands/userscripts.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Functions to execute a userscript.\"\"\"\n \n@@ -345,7 +330,6 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):\n                 self._filepath = handle.name\n         except OSError as e:\n             message.error(\"Error while creating tempfile: {}\".format(e))\n-            return\n \n \n class Error(Exception):\ndiff --git a/qutebrowser/completion/__init__.py b/qutebrowser/completion/__init__.py\nindex 0f86f9432..41e5c7f68 100644\n--- a/qutebrowser/completion/__init__.py\n+++ b/qutebrowser/completion/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Modules related to the command completion.\"\"\"\ndiff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py\nindex f8d69cc8f..846fa7c22 100644\n--- a/qutebrowser/completion/completer.py\n+++ b/qutebrowser/completion/completer.py\n@@ -1,33 +1,18 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Completer attached to a CompletionView.\"\"\"\n \n import dataclasses\n from typing import TYPE_CHECKING\n \n-from qutebrowser.qt.core import pyqtSlot, QObject, QTimer\n+from qutebrowser.qt.core import pyqtSlot, QObject\n \n from qutebrowser.config import config\n from qutebrowser.commands import parser, cmdexc\n from qutebrowser.misc import objects, split\n-from qutebrowser.utils import log, utils, debug, objreg\n+from qutebrowser.utils import log, utils, debug, objreg, usertypes\n from qutebrowser.completion.models import miscmodels\n from qutebrowser.completion import completionwidget\n if TYPE_CHECKING:\n@@ -64,7 +49,7 @@ class Completer(QObject):\n         super().__init__(parent)\n         self._cmd = cmd\n         self._win_id = win_id\n-        self._timer = QTimer()\n+        self._timer = usertypes.Timer()\n         self._timer.setSingleShot(True)\n         self._timer.setInterval(0)\n         self._timer.timeout.connect(self._update_completion)\n@@ -165,7 +150,6 @@ class Completer(QObject):\n                     # cursor is in a space between two existing words\n                     parts.insert(i, '')\n                 prefix = [x.strip() for x in parts[:i]]\n-                # pylint: disable-next=unnecessary-list-index-lookup\n                 center = parts[i].strip()\n                 # strip trailing whitespace included as a separate token\n                 postfix = [x.strip() for x in parts[i+1:] if not x.isspace()]\ndiff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py\nindex bc7f2f9c2..821a0a81e 100644\n--- a/qutebrowser/completion/completiondelegate.py\n+++ b/qutebrowser/completion/completiondelegate.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Completion item delegate for CompletionView.\n \n@@ -260,7 +245,7 @@ class CompletionItemDelegate(QStyledItemDelegate):\n         o = self._opt\n         o.rect = self._style.subElementRect(\n             QStyle.SubElement.SE_ItemViewItemFocusRect, self._opt, self._opt.widget)\n-        o.state |= int(QStyle.StateFlag.State_KeyboardFocusChange | QStyle.StateFlag.State_Item)\n+        o.state |= QStyle.StateFlag.State_KeyboardFocusChange | QStyle.StateFlag.State_Item\n         qtutils.ensure_valid(o.rect)\n         if state &amp; QStyle.StateFlag.State_Enabled:\n             cg = QPalette.ColorGroup.Normal\n@@ -293,6 +278,7 @@ class CompletionItemDelegate(QStyledItemDelegate):\n         self._opt = QStyleOptionViewItem(option)\n         self.initStyleOption(self._opt, index)\n         self._style = self._opt.widget.style()\n+        assert self._style is not None\n \n         self._get_textdoc(index)\n         assert self._doc is not None\ndiff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py\nindex a7bd41a8e..27e631662 100644\n--- a/qutebrowser/completion/completionwidget.py\n+++ b/qutebrowser/completion/completionwidget.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Completion view for statusbar command section.\n \n@@ -158,6 +143,15 @@ class CompletionView(QTreeView):\n         assert isinstance(model, completionmodel.CompletionModel), model\n         return model\n \n+    def _selection_model(self) -&gt; QItemSelectionModel:\n+        \"\"\"Get the current selection model.\n+\n+        Ensures the model is not None.\n+        \"\"\"\n+        model = self.selectionModel()\n+        assert model is not None\n+        return model\n+\n     @pyqtSlot(str)\n     def _on_config_changed(self, option):\n         if option in ['completion.height', 'completion.shrink']:\n@@ -171,7 +165,9 @@ class CompletionView(QTreeView):\n         column_widths = self._model().column_widths\n         pixel_widths = [(width * perc // 100) for perc in column_widths]\n \n-        delta = self.verticalScrollBar().sizeHint().width()\n+        bar = self.verticalScrollBar()\n+        assert bar is not None\n+        delta = bar.sizeHint().width()\n         for i, width in reversed(list(enumerate(pixel_widths))):\n             if width &gt; delta:\n                 pixel_widths[i] -= delta\n@@ -193,7 +189,7 @@ class CompletionView(QTreeView):\n             A QModelIndex.\n         \"\"\"\n         model = self._model()\n-        idx = self.selectionModel().currentIndex()\n+        idx = self._selection_model().currentIndex()\n         if not idx.isValid():\n             # No item selected yet\n             if upwards:\n@@ -225,7 +221,7 @@ class CompletionView(QTreeView):\n         Return:\n             A QModelIndex.\n         \"\"\"\n-        old_idx = self.selectionModel().currentIndex()\n+        old_idx = self._selection_model().currentIndex()\n         idx = old_idx\n         model = self._model()\n \n@@ -269,7 +265,7 @@ class CompletionView(QTreeView):\n         Return:\n             A QModelIndex.\n         \"\"\"\n-        idx = self.selectionModel().currentIndex()\n+        idx = self._selection_model().currentIndex()\n         model = self._model()\n         if not idx.isValid():\n             return self._next_idx(upwards).sibling(0, 0)\n@@ -277,18 +273,20 @@ class CompletionView(QTreeView):\n         direction = -1 if upwards else 1\n         while True:\n             idx = idx.sibling(idx.row() + direction, 0)\n-            if not idx.isValid() and upwards:\n+\n+            if idx.isValid():\n+                child = model.index(0, 0, idx)\n+                if child.isValid():\n+                    self.scrollTo(idx)  # scroll to ensure the category is visible\n+                    return child\n+            elif upwards:\n                 # wrap around to the first item of the last category\n                 return model.last_item().sibling(0, 0)\n-            elif not idx.isValid() and not upwards:\n+            else:\n                 # wrap around to the first item of the first category\n                 idx = model.first_item()\n                 self.scrollTo(idx.parent())\n                 return idx\n-            elif idx.isValid() and idx.child(0, 0).isValid():\n-                # scroll to ensure the category is visible\n-                self.scrollTo(idx)\n-                return idx.child(0, 0)\n \n         raise utils.Unreachable\n \n@@ -323,7 +321,7 @@ class CompletionView(QTreeView):\n         if not self._active:\n             return\n \n-        selmodel = self.selectionModel()\n+        selmodel = self._selection_model()\n         indices = {\n             'next': lambda: self._next_idx(upwards=False),\n             'prev': lambda: self._next_idx(upwards=True),\n@@ -363,9 +361,10 @@ class CompletionView(QTreeView):\n         Args:\n             model: The model to use.\n         \"\"\"\n-        if self.model() is not None and model is not self.model():\n-            self.model().deleteLater()\n-            self.selectionModel().deleteLater()\n+        old_model = self.model()\n+        if old_model is not None and model is not old_model:\n+            old_model.deleteLater()\n+            self._selection_model().deleteLater()\n \n         self.setModel(model)\n \n@@ -395,7 +394,7 @@ class CompletionView(QTreeView):\n         self.pattern = pattern\n         with debug.log_time(log.completion, 'Set pattern {}'.format(pattern)):\n             self._model().set_pattern(pattern)\n-            self.selectionModel().clear()\n+            self._selection_model().clear()\n             self._maybe_update_geometry()\n             self._maybe_show()\n \n@@ -426,16 +425,19 @@ class CompletionView(QTreeView):\n         confheight = str(config.val.completion.height)\n         if confheight.endswith('%'):\n             perc = int(confheight.rstrip('%'))\n-            height = self.window().height() * perc // 100\n+            window = self.window()\n+            assert window is not None\n+            height = window.height() * perc // 100\n         else:\n             height = int(confheight)\n         # Shrink to content size if needed and shrinking is enabled\n         if config.val.completion.shrink:\n+            bar = self.horizontalScrollBar()\n+            assert bar is not None\n             contents_height = (\n                 self.viewportSizeHint().height() +\n-                self.horizontalScrollBar().sizeHint().height())\n-            if contents_height &lt;= height:\n-                height = contents_height\n+                bar.sizeHint().height())\n+            height = min(height, contents_height)\n         # The width isn't really relevant as we're expanding anyways.\n         return QSize(-1, height)\n \ndiff --git a/qutebrowser/completion/models/__init__.py b/qutebrowser/completion/models/__init__.py\nindex f11f631d5..5a19f438f 100644\n--- a/qutebrowser/completion/models/__init__.py\n+++ b/qutebrowser/completion/models/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Models for the command completion.\"\"\"\ndiff --git a/qutebrowser/completion/models/completionmodel.py b/qutebrowser/completion/models/completionmodel.py\nindex ea491612a..6ddf27dcf 100644\n--- a/qutebrowser/completion/models/completionmodel.py\n+++ b/qutebrowser/completion/models/completionmodel.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Ryan Roden-Corrent (rcorre) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"A model that proxies access to one or more completion categories.\"\"\"\n \ndiff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py\nindex 6c85fbb29..bc7cfb078 100644\n--- a/qutebrowser/completion/models/configmodel.py\n+++ b/qutebrowser/completion/models/configmodel.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Functions that return config-related completion models.\"\"\"\n \ndiff --git a/qutebrowser/completion/models/filepathcategory.py b/qutebrowser/completion/models/filepathcategory.py\nindex 3bf241c30..1f4c04dea 100644\n--- a/qutebrowser/completion/models/filepathcategory.py\n+++ b/qutebrowser/completion/models/filepathcategory.py\n@@ -1,25 +1,10 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Completion category for filesystem paths.\n \n-NOTE: This module deliberatly uses os.path rather than pathlib, because of how\n+NOTE: This module deliberately uses os.path rather than pathlib, because of how\n it interacts with the completion, which operates on strings. For example, we\n need to be able to tell the difference between \"~/input\" and \"~/input/\". Also,\n if we get \"~/input\", we want to glob \"~/input*\" rather than \"~/input/*\" which\ndiff --git a/qutebrowser/completion/models/histcategory.py b/qutebrowser/completion/models/histcategory.py\nindex fa5c5f2aa..2e54eae91 100644\n--- a/qutebrowser/completion/models/histcategory.py\n+++ b/qutebrowser/completion/models/histcategory.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Ryan Roden-Corrent (rcorre) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"A completion category that queries the SQL history store.\"\"\"\n \ndiff --git a/qutebrowser/completion/models/listcategory.py b/qutebrowser/completion/models/listcategory.py\nindex e6756c1a3..f92679cc6 100644\n--- a/qutebrowser/completion/models/listcategory.py\n+++ b/qutebrowser/completion/models/listcategory.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Ryan Roden-Corrent (rcorre) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Completion category that uses a list of tuples as a data source.\"\"\"\n \n@@ -63,10 +48,13 @@ class ListCategory(QSortFilterProxyModel):\n             log.completion.warning(f\"Trimming {len(val)}-char pattern to 5000\")\n             val = val[:5000]\n         self._pattern = val\n-        val = re.sub(r' +', r' ', val)  # See #1919\n-        val = re.escape(val)\n-        val = val.replace(r'\\ ', '.*')\n-        rx = QRegularExpression(val, QRegularExpression.PatternOption.CaseInsensitiveOption)\n+\n+        # Positive lookahead per search term. This means that all search terms must\n+        # be matched but they can be matched anywhere in the string, so they can be\n+        # in any order. For example \"foo bar\" -&gt; \"(?=.*foo)(?=.*bar)\"\n+        re_pattern = \"^\" + \"\".join(f\"(?=.*{re.escape(term)})\" for term in val.split())\n+\n+        rx = QRegularExpression(re_pattern, QRegularExpression.PatternOption.CaseInsensitiveOption)\n         qtutils.ensure_valid(rx)\n         self.setFilterRegularExpression(rx)\n         self.invalidate()\ndiff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py\nindex 77072c720..ea3febe4d 100644\n--- a/qutebrowser/completion/models/miscmodels.py\n+++ b/qutebrowser/completion/models/miscmodels.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Functions that return miscellaneous completion models.\"\"\"\n \ndiff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py\nindex f64e0b570..0d6428348 100644\n--- a/qutebrowser/completion/models/urlmodel.py\n+++ b/qutebrowser/completion/models/urlmodel.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Function to return the url completion model for the `open` command.\"\"\"\n \ndiff --git a/qutebrowser/completion/models/util.py b/qutebrowser/completion/models/util.py\nindex 21dcf2e9f..492e1b2e5 100644\n--- a/qutebrowser/completion/models/util.py\n+++ b/qutebrowser/completion/models/util.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Ryan Roden-Corrent (rcorre) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utility functions for completion models.\"\"\"\n \ndiff --git a/qutebrowser/components/__init__.py b/qutebrowser/components/__init__.py\nindex a7613dd81..0c4d0c91b 100644\n--- a/qutebrowser/components/__init__.py\n+++ b/qutebrowser/components/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"qutebrowser \"extensions\" which only use the qutebrowser.api API.\"\"\"\ndiff --git a/qutebrowser/components/adblockcommands.py b/qutebrowser/components/adblockcommands.py\nindex 3b2cbe24d..065500bdd 100644\n--- a/qutebrowser/components/adblockcommands.py\n+++ b/qutebrowser/components/adblockcommands.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Commands relating to ad blocking.\"\"\"\n \ndiff --git a/qutebrowser/components/braveadblock.py b/qutebrowser/components/braveadblock.py\nindex d963df88e..a827eb546 100644\n--- a/qutebrowser/components/braveadblock.py\n+++ b/qutebrowser/components/braveadblock.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Functions related to the Brave adblocker.\"\"\"\n \n@@ -109,6 +94,7 @@ _RESOURCE_TYPE_STRINGS = {\n     ResourceType.plugin_resource: \"other\",\n     ResourceType.preload_main_frame: \"other\",\n     ResourceType.preload_sub_frame: \"other\",\n+    ResourceType.websocket: \"websocket\",\n     ResourceType.unknown: \"other\",\n     None: \"\",\n }\ndiff --git a/qutebrowser/components/caretcommands.py b/qutebrowser/components/caretcommands.py\nindex 8ab175012..15eb1938d 100644\n--- a/qutebrowser/components/caretcommands.py\n+++ b/qutebrowser/components/caretcommands.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Commands related to caret browsing.\"\"\"\n \ndiff --git a/qutebrowser/components/hostblock.py b/qutebrowser/components/hostblock.py\nindex f364269d6..672a530df 100644\n--- a/qutebrowser/components/hostblock.py\n+++ b/qutebrowser/components/hostblock.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Functions related to host blocking.\"\"\"\n \ndiff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py\nindex c31ec5e6c..e3ffb82d0 100644\n--- a/qutebrowser/components/misccommands.py\n+++ b/qutebrowser/components/misccommands.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # To allow count being documented\n # pylint: disable=differing-param-doc\n@@ -91,15 +76,18 @@ def _print_preview(tab: apitypes.Tab) -&gt; None:\n     diag.exec()\n \n \n-def _print_pdf(tab: apitypes.Tab, filename: str) -&gt; None:\n+def _print_pdf(tab: apitypes.Tab, path: pathlib.Path) -&gt; None:\n     \"\"\"Print to the given PDF file.\"\"\"\n     tab.printing.check_pdf_support()\n-    filename = os.path.expanduser(filename)\n-    directory = os.path.dirname(filename)\n-    if directory and not os.path.exists(directory):\n-        os.mkdir(directory)\n-    tab.printing.to_pdf(filename)\n-    _LOGGER.debug(\"Print to file: {}\".format(filename))\n+    path = path.expanduser()\n+\n+    try:\n+        path.parent.mkdir(parents=True, exist_ok=True)\n+    except OSError as e:\n+        raise cmdutils.CommandError(e)\n+\n+    tab.printing.to_pdf(path)\n+    _LOGGER.debug(f\"Print to file: {path}\")\n \n \n @cmdutils.register(name='print')\n@@ -107,7 +95,7 @@ def _print_pdf(tab: apitypes.Tab, filename: str) -&gt; None:\n @cmdutils.argument('pdf', flag='f', metavar='file')\n def printpage(tab: Optional[apitypes.Tab],\n               preview: bool = False, *,\n-              pdf: str = None) -&gt; None:\n+              pdf: Optional[pathlib.Path] = None) -&gt; None:\n     \"\"\"Print the current/[count]th tab.\n \n     Args:\n@@ -324,7 +312,7 @@ def debug_webaction(tab: apitypes.Tab, action: str, count: int = 1) -&gt; None:\n \n     Available actions:\n     https://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit)\n-    https://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine)\n+    https://doc.qt.io/qt-6/qwebenginepage.html#WebAction-enum (WebEngine)\n \n     Args:\n         action: The action to execute, e.g. MoveToNextChar.\n@@ -365,7 +353,7 @@ def message_error(text: str, rich: bool = False) -&gt; None:\n     Args:\n         text: The text to show.\n         rich: Render the given text as\n-              https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].\n+              https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].\n     \"\"\"\n     message.error(text, rich=rich)\n \n@@ -379,7 +367,7 @@ def message_info(text: str, count: int = 1, rich: bool = False) -&gt; None:\n         text: The text to show.\n         count: How many times to show the message.\n         rich: Render the given text as\n-              https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].\n+              https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].\n     \"\"\"\n     for _ in range(count):\n         message.info(text, rich=rich)\n@@ -392,7 +380,7 @@ def message_warning(text: str, rich: bool = False) -&gt; None:\n     Args:\n         text: The text to show.\n         rich: Render the given text as\n-              https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].\n+              https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].\n     \"\"\"\n     message.warning(text, rich=rich)\n \n@@ -405,6 +393,7 @@ def debug_crash(typ: str = 'exception') -&gt; None:\n     Args:\n         typ: either 'exception' or 'segfault'.\n     \"\"\"\n+    # pylint: disable=broad-exception-raised\n     if typ == 'segfault':\n         os.kill(os.getpid(), signal.SIGSEGV)\n         raise Exception(\"Segfault failed (wat.)\")\ndiff --git a/qutebrowser/components/readlinecommands.py b/qutebrowser/components/readlinecommands.py\nindex f6ee31e84..a9626637d 100644\n--- a/qutebrowser/components/readlinecommands.py\n+++ b/qutebrowser/components/readlinecommands.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Bridge to provide readline-like shortcuts for QLineEdits.\"\"\"\n \ndiff --git a/qutebrowser/components/scrollcommands.py b/qutebrowser/components/scrollcommands.py\nindex 2287041d7..3ee525535 100644\n--- a/qutebrowser/components/scrollcommands.py\n+++ b/qutebrowser/components/scrollcommands.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Scrolling-related commands.\"\"\"\n \n@@ -47,7 +32,7 @@ def scroll_px(tab: apitypes.Tab, dx: int, dy: int, count: int = 1) -&gt; None:\n def scroll(tab: apitypes.Tab, direction: str, count: int = 1) -&gt; None:\n     \"\"\"Scroll the current tab in the given direction.\n \n-    Note you can use `:run-with-count` to have a keybinding with a bigger\n+    Note you can use `:cmd-run-with-count` to have a keybinding with a bigger\n     scroll increment.\n \n     Args:\ndiff --git a/qutebrowser/components/utils/blockutils.py b/qutebrowser/components/utils/blockutils.py\nindex 828a87952..369b0eee5 100644\n--- a/qutebrowser/components/utils/blockutils.py\n+++ b/qutebrowser/components/utils/blockutils.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Code that is shared between the host blocker and Brave ad blocker.\"\"\"\n \ndiff --git a/qutebrowser/components/zoomcommands.py b/qutebrowser/components/zoomcommands.py\nindex 72715b730..05e2bebaa 100644\n--- a/qutebrowser/components/zoomcommands.py\n+++ b/qutebrowser/components/zoomcommands.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Zooming-related commands.\"\"\"\n \ndiff --git a/qutebrowser/config/__init__.py b/qutebrowser/config/__init__.py\nindex 516879e79..4688cc758 100644\n--- a/qutebrowser/config/__init__.py\n+++ b/qutebrowser/config/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Modules related to the configuration.\"\"\"\ndiff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py\nindex 5acba5b99..cb7fe77b3 100644\n--- a/qutebrowser/config/config.py\n+++ b/qutebrowser/config/config.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Configuration storage and config-related utilities.\"\"\"\n \n@@ -44,7 +29,7 @@ key_instance = cast('KeyConfig', None)\n cache = cast('configcache.ConfigCache', None)\n \n # Keeping track of all change filters to validate them later.\n-change_filters = []\n+change_filters: List[\"change_filter\"] = []\n \n # Sentinel\n UNSET = object()\n@@ -109,7 +94,7 @@ class change_filter:  # noqa: N801,N806 pylint: disable=invalid-name\n         and calls the wrapped function if we are.\n \n         We assume the function passed doesn't take any parameters. However, it\n-        could take a \"self\" argument, so we can't cleary express this in the\n+        could take a \"self\" argument, so we can't clearly express this in the\n         type above.\n \n         Args:\n@@ -169,14 +154,14 @@ class KeyConfig:\n         return bindings\n \n     def _implied_cmd(self, cmdline: str) -&gt; Optional[str]:\n-        \"\"\"Return cmdline, or the implied cmd if cmdline is a set-cmd-text.\"\"\"\n+        \"\"\"Return cmdline, or the implied cmd if cmdline is a cmd-set-text.\"\"\"\n         try:\n             results = parser.CommandParser().parse_all(cmdline)\n         except cmdexc.NoSuchCommandError:\n             return None\n \n         result = results[0]\n-        if result.cmd.name != \"set-cmd-text\":\n+        if result.cmd.name not in [\"set-cmd-text\", \"cmd-set-text\"]:\n             return cmdline\n         if not result.args:\n             return None  # doesn't look like this sets a command\n@@ -188,7 +173,7 @@ class KeyConfig:\n     def get_reverse_bindings_for(self, mode: str) -&gt; '_ReverseBindings':\n         \"\"\"Get a dict of commands to a list of bindings for the mode.\n \n-        This is intented for user-facing display of keybindings.\n+        This is intended for user-facing display of keybindings.\n         As such, bindings for 'set-cmd-text [flags] : ...' are translated\n         to ' ...', as from the user's perspective these keys behave like\n         bindings for '' (that allow for further input before running).\n@@ -560,15 +545,18 @@ class Config(QObject):\n                 log.config.debug(\"{} was mutated, updating\".format(name))\n                 self.set_obj(name, new_value, save_yaml=save_yaml)\n \n-    def dump_userconfig(self) -&gt; str:\n+    def dump_userconfig(self, *, include_hidden: bool = False) -&gt; str:\n         \"\"\"Get the part of the config which was changed by the user.\n \n+        Args:\n+            include_hidden: Include default scoped configs.\n+\n         Return:\n             The changed config part as string.\n         \"\"\"\n         lines: List[str] = []\n         for values in sorted(self, key=lambda v: v.opt.name):\n-            lines += values.dump()\n+            lines += values.dump(include_hidden=include_hidden)\n \n         if not lines:\n             return ''\ndiff --git a/qutebrowser/config/configcache.py b/qutebrowser/config/configcache.py\nindex 54915fb2b..9e76466d9 100644\n--- a/qutebrowser/config/configcache.py\n+++ b/qutebrowser/config/configcache.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Jay Kamat \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Jay Kamat \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Implementation of a basic config cache.\"\"\"\n \ndiff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py\nindex feeaf2169..c4065ceb9 100644\n--- a/qutebrowser/config/configcommands.py\n+++ b/qutebrowser/config/configcommands.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Commands related to the configuration.\"\"\"\n \n@@ -23,7 +8,7 @@ import os.path\n import contextlib\n from typing import TYPE_CHECKING, Iterator, List, Optional, Any, Tuple\n \n-from qutebrowser.qt.core import QUrl\n+from qutebrowser.qt.core import QUrl, QUrlQuery\n \n from qutebrowser.api import cmdutils\n from qutebrowser.completion.models import configmodel\n@@ -281,9 +266,18 @@ class ConfigCommands:\n \n     @cmdutils.register(instance='config-commands')\n     @cmdutils.argument('win_id', value=cmdutils.Value.win_id)\n-    def config_diff(self, win_id: int) -&gt; None:\n-        \"\"\"Show all customized options.\"\"\"\n+    def config_diff(self, win_id: int, include_hidden: bool = False) -&gt; None:\n+        \"\"\"Show all customized options.\n+\n+        Args:\n+            include_hidden: Also include internal qutebrowser settings.\n+        \"\"\"\n         url = QUrl('qute://configdiff')\n+        if include_hidden:\n+            query = QUrlQuery()\n+            query.addQueryItem(\"include_hidden\", \"true\")\n+            url.setQuery(query)\n+\n         tabbed_browser = objreg.get('tabbed-browser',\n                                     scope='window', window=win_id)\n         tabbed_browser.load_url(url, newtab=False)\ndiff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py\nindex ec4efc375..2377841ef 100644\n--- a/qutebrowser/config/configdata.py\n+++ b/qutebrowser/config/configdata.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Configuration data for config.py.\n \n@@ -25,7 +10,7 @@ DATA: A dict of Option objects after init() has been called.\n \"\"\"\n \n from typing import (Any, Dict, Iterable, List, Mapping, MutableMapping, Optional,\n-                    Sequence, Tuple, Union, cast)\n+                    Sequence, Tuple, Union, NoReturn, cast)\n import functools\n import dataclasses\n \n@@ -72,7 +57,7 @@ class Migrations:\n     deleted: List[str] = dataclasses.field(default_factory=list)\n \n \n-def _raise_invalid_node(name: str, what: str, node: Any) -&gt; None:\n+def _raise_invalid_node(name: str, what: str, node: Any) -&gt; NoReturn:\n     \"\"\"Raise an exception for an invalid configdata YAML node.\n \n     Args:\n@@ -109,6 +94,7 @@ def _parse_yaml_type(\n         _raise_invalid_node(name, 'type', node)\n \n     try:\n+        # pylint: disable=possibly-used-before-assignment\n         typ = getattr(configtypes, type_name)\n     except AttributeError:\n         raise AttributeError(\"Did not find type {} for {}\".format(\n@@ -155,12 +141,13 @@ def _parse_yaml_backends_dict(\n \n     # The value associated to the key, and whether we should add that backend\n     # or not.\n+\n     conditionals = {\n         True: True,\n         False: False,\n-        'Qt 5.13': qtutils.version_check('5.13'),\n-        'Qt 5.14': qtutils.version_check('5.14'),\n         'Qt 5.15': qtutils.version_check('5.15'),\n+        'Qt 6.2': qtutils.version_check('6.2'),\n+        'Qt 6.3': qtutils.version_check('6.3'),\n     }\n     for key in sorted(node.keys()):\n         if conditionals[node[key]]:\ndiff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml\nindex 8a676145d..322f88f6c 100644\n--- a/qutebrowser/config/configdata.yml\n+++ b/qutebrowser/config/configdata.yml\n@@ -65,9 +65,6 @@ search.incremental:\n search.wrap:\n   type: Bool\n   default: true\n-  backend:\n-    QtWebEngine: Qt 5.14\n-    QtWebKit: true\n   desc: &gt;-\n     Wrap around at the top and bottom of the page when advancing through text\n     matches using `:search-next` and `:search-prev`.\n@@ -275,7 +272,7 @@ qt.chromium.process_model:\n     See the following pages for more details:\n \n       - https://www.chromium.org/developers/design-documents/process-models\n-      - https://doc.qt.io/qt-5/qtwebengine-features.html#process-models\n+      - https://doc.qt.io/qt-6/qtwebengine-features.html#process-models\n \n qt.low_end_device_mode:\n   renamed: qt.chromium.low_end_device_mode\n@@ -330,6 +327,23 @@ qt.chromium.sandboxing:\n     - https://chromium.googlesource.com/chromium/src/\\+/HEAD/docs/design/sandbox_faq.md[FAQ (Windows-centric)]\n   # yamllint enable rule:line-length\n \n+qt.chromium.experimental_web_platform_features:\n+  type:\n+    name: String\n+    valid_values:\n+      - always: Enable experimental web platform features.\n+      - auto: Enable experimental web platform features when using Qt 5.\n+      - never: Disable experimental web platform features.\n+  default: auto\n+  backend: QtWebEngine\n+  restart: true\n+  desc: &gt;-\n+    Enables Web Platform features that are in development.\n+\n+    This passes the `--enable-experimental-web-platform-features` flag to\n+    Chromium. By default, this is enabled with Qt 5 to maximize compatibility\n+    despite an aging Chromium base.\n+\n qt.highdpi:\n   type: Bool\n   default: false\n@@ -337,8 +351,8 @@ qt.highdpi:\n   desc: &gt;-\n     Turn on Qt HighDPI scaling.\n \n-    This is equivalent to setting QT_AUTO_SCREEN_SCALE_FACTOR=1 or\n-    QT_ENABLE_HIGHDPI_SCALING=1 (Qt &gt;= 5.14) in the environment.\n+    This is equivalent to setting QT_ENABLE_HIGHDPI_SCALING=1 (Qt &gt;= 5.14) in\n+    the environment.\n \n     It's off by default as it can cause issues with some bitmap fonts.\n     As an alternative to this, it's possible to set font sizes and the\n@@ -371,6 +385,25 @@ qt.workarounds.locale:\n     However, It is expected that distributions shipping QtWebEngine 5.15.3\n     follow up with a proper fix soon, so it is disabled by default.\n \n+qt.workarounds.disable_accelerated_2d_canvas:\n+  type:\n+    name: String\n+    valid_values:\n+      - always: Disable accelerated 2d canvas\n+      - auto: Disable on Qt6 &lt; 6.6.0, enable otherwise\n+      - never: Enable accelerated 2d canvas\n+  default: auto\n+  backend: QtWebEngine\n+  restart: true\n+  desc: &gt;-\n+    Disable accelerated 2d canvas to avoid graphical glitches.\n+\n+    On some setups graphical issues can occur on sites like Google sheets\n+    and PDF.js. These don't occur when accelerated 2d canvas is turned off,\n+    so we do that by default.\n+\n+    So far these glitches only occur on some Intel graphics devices.\n+\n ## auto_save\n \n auto_save.interval:\n@@ -421,12 +454,15 @@ content.canvas_reading:\n   default: true\n   type: Bool\n   backend: QtWebEngine\n-  restart: true\n+  supports_pattern: true\n   desc: &gt;-\n     Allow websites to read canvas elements.\n \n     Note this is needed for some websites to work properly.\n \n+    On QtWebEngine &lt; 6.6, this setting requires a restart and does not support\n+    URL patterns, only the global setting is applied.\n+\n # Defaults from QWebSettings::QWebSettings() in\n # qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp\n \n@@ -577,9 +613,7 @@ content.frame_flattening:\n content.prefers_reduced_motion:\n   default: false\n   type: Bool\n-  backend:\n-    QtWebEngine: Qt 5.14\n-    QtWebKit: false\n+  backend: QtWebEngine\n   restart: true\n   desc: &gt;-\n     Request websites to minimize non-essentials animations and motion.\n@@ -611,20 +645,14 @@ content.site_specific_quirks.skip:\n       - js-whatsapp-web\n       - js-discord\n       - js-string-replaceall\n-      - js-globalthis\n-      - js-object-fromentries\n+      - js-array-at\n       - misc-krunker\n       - misc-mathml-darkmode\n     none_ok: true\n-  default: [\"js-string-replaceall\"]\n+  default: []\n   desc: &gt;-\n     Disable a list of named quirks.\n \n-    The js-string-replaceall quirk is needed for Nextcloud Calendar &lt; 2.2.0 with\n-    QtWebEngine &lt; 5.15.3. However, the workaround is not fully compliant to the\n-    ECMAScript spec and might cause issues on other websites, so it's disabled by\n-    default.\n-\n # emacs: '\n \n content.geolocation:\n@@ -684,7 +712,8 @@ content.headers.referer:\n   type:\n     name: String\n     valid_values:\n-      - always: \"Always send the Referer.\"\n+      - always: \"Always send the Referer. With QtWebEngine 6.2+, this value is\n+          unavailable and will act like `same-domain`.\"\n       - never: \"Never send the Referer. This is not recommended, as some sites\n           may break.\"\n       - same-domain: \"Only send the Referer for the same domain. This will\n@@ -725,15 +754,15 @@ content.headers.user_agent:\n       # 'ua_fetch.py'\n       # Vim-protip: Place your cursor below this comment and run\n       # :r!python scripts/dev/ua_fetch.py\n-      - - \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,\n-          like Gecko) Chrome/97.0.4692.99 Safari/537.36\"\n-        - Chrome 97 Win10\n       - - \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36\n-          (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36\"\n-        - Chrome 97 macOS\n+          (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36\"\n+        - Chrome 117 macOS\n+      - - \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,\n+          like Gecko) Chrome/117.0.0.0 Safari/537.36\"\n+        - Chrome 117 Win10\n       - - \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like\n-          Gecko) Chrome/97.0.4692.99 Safari/537.36\"\n-        - Chrome 97 Linux\n+          Gecko) Chrome/117.0.0.0 Safari/537.36\"\n+        - Chrome 117 Linux\n   supports_pattern: true\n   desc: |\n     User agent to send.\n@@ -950,12 +979,14 @@ content.javascript.log_message.levels:\n     keytype: String\n     valtype:\n       name: FlagList\n+      none_ok: true\n       valid_values:\n         - info: Show JS info as messages.\n         - warning: Show JS warnings as messages.\n         - error: Show JS errors as messages.\n   default:\n     \"qute:*\": [\"error\"]\n+    \"userscript:GM-*\": []\n     \"userscript:*\": [\"error\"]\n   desc: &gt;-\n     Javascript message sources/levels to show in the qutebrowser UI.\n@@ -964,8 +995,8 @@ content.javascript.log_message.levels:\n     pattern given in the key, and is from one of the levels listed as value,\n     it's surfaced as a message in the qutebrowser UI.\n \n-    By default, errors happening in qutebrowser internally or in userscripts are\n-    shown to the user.\n+    By default, errors happening in qutebrowser internally are shown to the\n+    user.\n \n content.javascript.log_message.excludes:\n   type:\n@@ -977,7 +1008,7 @@ content.javascript.log_message.excludes:\n       valtype: String\n   default:\n     \"userscript:_qute_stylesheet\":\n-      - \"Refused to apply inline style because it violates the following Content\n+      - \"*Refused to apply inline style because it violates the following Content\n         Security Policy directive: *\"\n   desc: &gt;-\n     Javascript messages to *not* show in the UI, despite a corresponding\n@@ -1000,6 +1031,31 @@ content.javascript.prompt:\n   type: Bool\n   desc: Show javascript prompts.\n \n+content.javascript.legacy_touch_events:\n+  type:\n+    name: String\n+    valid_values:\n+      - always: Legacy touch events are always enabled. This might cause some\n+            websites to assume a mobile device.\n+      - auto: Legacy touch events are only enabled if a touch screen was\n+            detected on startup.\n+      - never: Legacy touch events are always disabled.\n+  default: never\n+  backend: QtWebEngine\n+  restart: true\n+  desc: &gt;-\n+    Enables the legacy touch event feature.\n+\n+    This affects JS APIs such as:\n+\n+    - ontouch* members on window, document, Element\n+    - document.createTouch, document.createTouchList\n+    - document.createEvent(\"TouchEvent\")\n+\n+    Newer Chromium versions have those disabled by default:\n+    https://bugs.chromium.org/p/chromium/issues/detail?id=392584\n+    https://groups.google.com/a/chromium.org/g/blink-dev/c/KV6kqDJpYiE\n+\n content.local_content_can_access_remote_urls:\n   default: false\n   type: Bool\n@@ -1056,9 +1112,6 @@ content.notifications.enabled:\n   default: ask\n   type: BoolAsk\n   supports_pattern: true\n-  backend:\n-    QtWebEngine: Qt 5.13\n-    QtWebKit: true\n   desc: Allow websites to show notifications.\n \n content.notifications.presenter:\n@@ -1070,7 +1123,6 @@ content.notifications.presenter:\n             without showing error messages.\n       - qt: Use Qt's native notification presenter, based on a system tray icon.\n             Switching from or to this value requires a restart of qutebrowser.\n-            Recommended over `systray` on PyQt 5.14.\n       - libnotify: Shows messages via DBus in a libnotify-compatible way. If DBus isn't\n             available, falls back to `systray` or `messages`, but shows an error\n             message.\n@@ -1082,16 +1134,12 @@ content.notifications.presenter:\n             aren't available.\n       - herbe: (experimental!) Show notifications using herbe (github.com/dudik/herbe).\n             Most notification features aren't available.\n-  backend:\n-    QtWebEngine: Qt 5.14\n-    QtWebKit: false\n+  backend: QtWebEngine\n   desc: &gt;-\n     What notification presenter to use for web notifications.\n \n     Note that not all implementations support all features of notifications:\n \n-    - With PyQt 5.14, any setting other than `qt` does not support  the `click` and\n-      `close` events, as well as the `tag` option to replace existing notifications.\n     - The `qt` and `systray` options only support showing one notification at the time\n       and ignore the `tag` option to replace existing notifications.\n     - The `herbe` option only supports showing one notification at the time and doesn't\n@@ -1103,9 +1151,7 @@ content.notifications.show_origin:\n   default: true\n   type: Bool\n   supports_pattern: true\n-  backend:\n-    QtWebEngine: Qt 5.14\n-    QtWebKit: false\n+  backend: QtWebEngine\n   desc: &gt;-\n     Whether to show the origin URL for notifications.\n \n@@ -1519,11 +1565,10 @@ fileselect.handler:\n       - default: \"Use the default file selector.\"\n       - external: \"Use an external command.\"\n   desc: &gt;-\n-      Handler for selecting file(s) in forms.\n-      If `external`, then the commands specified by\n-      `fileselect.single_file.command` and\n-      `fileselect.multiple_files.command` are used to select one or\n-      multiple files respectively.\n+      Handler for selecting file(s) in forms. If `external`, then the commands\n+      specified by `fileselect.single_file.command`,\n+      `fileselect.multiple_files.command` and `fileselect.folder.command` are\n+      used to select one file, multiple files, and folders, respectively.\n \n fileselect.single_file.command:\n   type:\n@@ -1739,10 +1784,12 @@ hints.selectors:\n       - '[role=\"button\"]'\n       - '[role=\"tab\"]'\n       - '[role=\"checkbox\"]'\n+      - '[role=\"switch\"]'\n       - '[role=\"menuitem\"]'\n       - '[role=\"menuitemcheckbox\"]'\n       - '[role=\"menuitemradio\"]'\n       - '[role=\"treeitem\"]'\n+      - '[aria-haspopup]'\n       - '[ng-click]'\n       - '[ngClick]'\n       - '[data-ng-click]'\n@@ -1899,9 +1946,7 @@ input.spatial_navigation:\n input.media_keys:\n   default: true\n   type: Bool\n-  backend:\n-    QtWebEngine: Qt 5.14\n-    QtWebKit: false\n+  backend: QtWebEngine\n   restart: true\n   desc: &gt;-\n     Whether the underlying Chromium should handle media keys.\n@@ -2556,6 +2601,7 @@ url.yank_ignored_parameters:\n     - utm_campaign\n     - utm_term\n     - utm_content\n+    - utm_name\n   desc: URL parameters to strip with `:yank url`.\n \n ## window\n@@ -2741,6 +2787,26 @@ colors.contextmenu.bg:\n colors.contextmenu.fg:\n   renamed: colors.contextmenu.menu.fg\n \n+colors.tooltip.bg:\n+  type:\n+    name: QssColor\n+    none_ok: true\n+  default: null\n+  desc: &gt;-\n+    Background color of tooltips.\n+\n+    If set to null, the Qt default is used.\n+\n+colors.tooltip.fg:\n+  type:\n+    name: QssColor\n+    none_ok: true\n+  default: null\n+  desc: &gt;-\n+    Foreground color of tooltips.\n+\n+    If set to null, the Qt default is used.\n+\n colors.contextmenu.menu.bg:\n   type:\n     name: QssColor\n@@ -3195,9 +3261,7 @@ colors.webpage.preferred_color_scheme:\n \n     The \"auto\" value is broken on QtWebEngine 5.15.2 due to a Qt bug. There, it will\n     fall back to \"light\" unconditionally.\n-  backend:\n-    QtWebEngine: Qt 5.14\n-    QtWebKit: false\n+  backend: QtWebEngine\n   restart: true\n \n ## dark mode\n@@ -3208,28 +3272,23 @@ colors.webpage.darkmode.enabled:\n   desc: &gt;-\n     Render all web contents using a dark theme.\n \n-    Example configurations from Chromium's `chrome://flags`:\n+    On QtWebEngine &lt; 6.7, this setting requires a restart and does not support\n+    URL patterns, only the global setting is applied.\n \n+    Example configurations from Chromium's `chrome://flags`:\n \n     - \"With simple HSL/CIELAB/RGB-based inversion\": Set\n-      `colors.webpage.darkmode.algorithm` accordingly.\n-\n-    - \"With selective image inversion\": Set\n-      `colors.webpage.darkmode.policy.images` to `smart`.\n-\n-    - \"With selective inversion of non-image elements\": Set\n-      `colors.webpage.darkmode.threshold.text` to 150 and\n-      `colors.webpage.darkmode.threshold.background` to 205.\n+      `colors.webpage.darkmode.algorithm` accordingly, and\n+      set `colors.webpage.darkmode.policy.images` to `never`.\n \n-    - \"With selective inversion of everything\": Combines the two variants\n-      above.\n-  restart: true\n+    - \"With selective image inversion\": qutebrowser default settings.\n+  supports_pattern: true\n   backend: QtWebEngine\n \n colors.webpage.darkmode.algorithm:\n   default: lightness-cielab\n   desc: &gt;-\n-    Which algorithm to use for modifying how colors are rendered with darkmode.\n+    Which algorithm to use for modifying how colors are rendered with dark mode.\n \n     The `lightness-cielab` value was added with QtWebEngine 5.14 and is treated\n     like `lightness-hsl` with older QtWebEngine versions.\n@@ -3271,13 +3330,11 @@ colors.webpage.darkmode.policy.images:\n       - never: Never apply dark mode filter to any images.\n       - smart: \"Apply dark mode based on image content. Not available with Qt\n         5.15.0.\"\n+      - smart-simple: \"On QtWebEngine 6.6, use a simpler algorithm for smart mode (based\n+      on numbers of colors and transparency), rather than an ML-based model.\n+      Same as 'smart' on older QtWebEnigne versions.\"\n   desc: &gt;-\n       Which images to apply dark mode to.\n-\n-      With QtWebEngine 5.15.0, this setting can cause frequent renderer process\n-      crashes due to a\n-      https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug\n-      in Qt].\n   restart: true\n   backend: QtWebEngine\n \n@@ -3294,11 +3351,12 @@ colors.webpage.darkmode.policy.page:\n     The underlying Chromium setting has been removed in QtWebEngine 5.15.3, thus this\n     setting is ignored there. Instead, every element is now classified individually.\n   restart: true\n-  backend:\n-    QtWebEngine: Qt 5.14\n-    QtWebKit: false\n+  backend: QtWebEngine\n \n colors.webpage.darkmode.threshold.text:\n+  renamed: colors.webpage.darkmode.threshold.foreground\n+\n+colors.webpage.darkmode.threshold.foreground:\n   default: 256\n   type:\n     name: Int\n@@ -3311,9 +3369,7 @@ colors.webpage.darkmode.threshold.text:\n       above it will be left as in the original, non-dark-mode page. Set to 256\n       to always invert text color or to 0 to never invert text color.\n   restart: true\n-  backend:\n-    QtWebEngine: Qt 5.14\n-    QtWebKit: false\n+  backend: QtWebEngine\n \n colors.webpage.darkmode.threshold.background:\n   default: 0\n@@ -3329,39 +3385,10 @@ colors.webpage.darkmode.threshold.background:\n       256 to never invert the color or to 0 to always invert it.\n \n       Note: This behavior is the opposite of\n-      `colors.webpage.darkmode.threshold.text`!\n-  restart: true\n-  backend:\n-    QtWebEngine: Qt 5.14\n-    QtWebKit: false\n-\n-colors.webpage.darkmode.grayscale.all:\n-  default: false\n-  type: Bool\n-  desc: &gt;-\n-    Render all colors as grayscale.\n-\n-    This only has an effect when `colors.webpage.darkmode.algorithm` is set to\n-    `lightness-hsl` or `brightness-rgb`.\n+      `colors.webpage.darkmode.threshold.foreground`!\n   restart: true\n   backend: QtWebEngine\n \n-colors.webpage.darkmode.grayscale.images:\n-  default: 0.0\n-  type:\n-    name: Float\n-    minval: 0.0\n-    maxval: 1.0\n-  desc: &gt;-\n-    Desaturation factor for images in dark mode.\n-\n-    If set to 0, images are left as-is. If set to 1, images are completely\n-    grayscale. Values between 0 and 1 desaturate the colors accordingly.\n-  restart: true\n-  backend:\n-    QtWebEngine: Qt 5.14\n-    QtWebKit: false\n-\n # emacs: '\n \n ## fonts\n@@ -3404,6 +3431,16 @@ fonts.completion.category:\n   type: Font\n   desc: Font used in the completion categories.\n \n+fonts.tooltip:\n+  type:\n+    name: Font\n+    none_ok: true\n+  default: null\n+  desc: &gt;-\n+    Font used for tooltips.\n+\n+    If set to null, the Qt default is used.\n+\n fonts.contextmenu:\n   type:\n     name: Font\n@@ -3602,17 +3639,17 @@ bindings.default:\n   default:\n     normal:\n       : clear-keychain ;; search ;; fullscreen --leave\n-      o: set-cmd-text -s :open\n-      go: set-cmd-text :open {url:pretty}\n-      O: set-cmd-text -s :open -t\n-      gO: set-cmd-text :open -t -r {url:pretty}\n-      xo: set-cmd-text -s :open -b\n-      xO: set-cmd-text :open -b -r {url:pretty}\n-      wo: set-cmd-text -s :open -w\n-      wO: set-cmd-text :open -w {url:pretty}\n-      /: set-cmd-text /\n-      ?: set-cmd-text ?\n-      \":\": \"set-cmd-text :\"\n+      o: cmd-set-text -s :open\n+      go: cmd-set-text :open {url:pretty}\n+      O: cmd-set-text -s :open -t\n+      gO: cmd-set-text :open -t -r {url:pretty}\n+      xo: cmd-set-text -s :open -b\n+      xO: cmd-set-text :open -b -r {url:pretty}\n+      wo: cmd-set-text -s :open -w\n+      wO: cmd-set-text :open -w {url:pretty}\n+      /: cmd-set-text /\n+      ?: cmd-set-text ?\n+      \":\": \"cmd-set-text :\"\n       ga: open -t\n       : open -t\n       : open -w\n@@ -3622,7 +3659,7 @@ bindings.default:\n       : close\n       D: tab-close -o\n       co: tab-only\n-      T: set-cmd-text -sr :tab-focus\n+      T: cmd-set-text -sr :tab-focus\n       gm: tab-move\n       gK: tab-move -\n       gJ: tab-move +\n@@ -3694,17 +3731,17 @@ bindings.default:\n       wp: open -w -- {clipboard}\n       wP: open -w -- {primary}\n       m: quickmark-save\n-      b: set-cmd-text -s :quickmark-load\n-      B: set-cmd-text -s :quickmark-load -t\n-      wb: set-cmd-text -s :quickmark-load -w\n+      b: cmd-set-text -s :quickmark-load\n+      B: cmd-set-text -s :quickmark-load -t\n+      wb: cmd-set-text -s :quickmark-load -w\n       M: bookmark-add\n-      gb: set-cmd-text -s :bookmark-load\n-      gB: set-cmd-text -s :bookmark-load -t\n-      wB: set-cmd-text -s :bookmark-load -w\n+      gb: cmd-set-text -s :bookmark-load\n+      gB: cmd-set-text -s :bookmark-load -t\n+      wB: cmd-set-text -s :bookmark-load -w\n       sf: save\n-      ss: set-cmd-text -s :set\n-      sl: set-cmd-text -s :set -t\n-      sk: set-cmd-text -s :bind\n+      ss: cmd-set-text -s :set\n+      sl: cmd-set-text -s :set -t\n+      sk: cmd-set-text -s :bind\n       -: zoom-out\n       +: zoom-in\n       =: zoom\n@@ -3727,7 +3764,7 @@ bindings.default:\n       ad: download-cancel\n       cd: download-clear\n       gf: view-source\n-      gt: set-cmd-text -s :tab-select\n+      gt: cmd-set-text -s :tab-select\n       : tab-focus last\n       : nop\n       : tab-focus last\n@@ -3760,7 +3797,7 @@ bindings.default:\n       Sh: history\n       : selection-follow\n       : selection-follow -t\n-      .: repeat-command\n+      .: cmd-repeat-last\n       : tab-pin\n       : tab-mute\n       gD: tab-give\ndiff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py\nindex 58e5ad67a..4c8291580 100644\n--- a/qutebrowser/config/configexc.py\n+++ b/qutebrowser/config/configexc.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Exceptions related to config parsing.\"\"\"\n \ndiff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py\nindex e7d3d4de1..0680cd0e7 100644\n--- a/qutebrowser/config/configfiles.py\n+++ b/qutebrowser/config/configfiles.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Configuration files residing on disk.\"\"\"\n \n@@ -91,6 +76,7 @@ class StateConfig(configparser.ConfigParser):\n         self.qt_version_changed = False\n         self.qtwe_version_changed = False\n         self.qutebrowser_version_changed = VersionChange.unknown\n+        self.chromium_version_changed = VersionChange.unknown\n         self._set_changed_attributes()\n \n         for sect in ['general', 'geometry', 'inspector']:\n@@ -103,25 +89,50 @@ class StateConfig(configparser.ConfigParser):\n             ('general', 'fooled'),\n             ('general', 'backend-warning-shown'),\n             ('general', 'old-qt-warning-shown'),\n+            ('general', 'serviceworker_workaround'),\n             ('geometry', 'inspector'),\n         ]\n         for sect, key in deleted_keys:\n             self[sect].pop(key, None)\n \n-        self['general']['qt_version'] = qVersion()\n+        qt_version = qVersion()\n+        assert qt_version is not None\n+        self['general']['qt_version'] = qt_version\n         self['general']['qtwe_version'] = self._qtwe_version_str()\n+        self['general']['chromium_version'] = self._chromium_version_str()\n         self['general']['version'] = qutebrowser.__version__\n \n-    def _qtwe_version_str(self) -&gt; str:\n-        \"\"\"Get the QtWebEngine version string.\n+    def _has_webengine(self) -&gt; bool:\n+        \"\"\"Check if QtWebEngine is available.\n \n         Note that it's too early to use objects.backend here...\n         \"\"\"\n         try:\n-            import qutebrowser.qt.webenginewidgets  # pylint: disable=unused-import\n+            # pylint: disable=unused-import,redefined-outer-name\n+            import qutebrowser.qt.webenginewidgets\n         except ImportError:\n+            return False\n+        return True\n+\n+    def _qtwe_versions(self) -&gt; Optional[version.WebEngineVersions]:\n+        \"\"\"Get the QtWebEngine versions.\"\"\"\n+        if not self._has_webengine():\n+            return None\n+        return version.qtwebengine_versions(avoid_init=True)\n+\n+    def _qtwe_version_str(self) -&gt; str:\n+        \"\"\"Get the QtWebEngine version string.\"\"\"\n+        versions = self._qtwe_versions()\n+        if versions is None:\n+            return 'no'\n+        return str(versions.webengine)\n+\n+    def _chromium_version_str(self) -&gt; str:\n+        \"\"\"Get the Chromium major version string.\"\"\"\n+        versions = self._qtwe_versions()\n+        if versions is None:\n             return 'no'\n-        return str(version.qtwebengine_versions(avoid_init=True).webengine)\n+        return str(versions.chromium_major)\n \n     def _set_changed_attributes(self) -&gt; None:\n         \"\"\"Set qt_version_changed/qutebrowser_version_changed attributes.\n@@ -139,10 +150,14 @@ class StateConfig(configparser.ConfigParser):\n         old_qtwe_version = self['general'].get('qtwe_version', None)\n         self.qtwe_version_changed = old_qtwe_version != self._qtwe_version_str()\n \n+        self._set_qutebrowser_changed_attribute()\n+        self._set_chromium_changed_attribute()\n+\n+    def _set_qutebrowser_changed_attribute(self) -&gt; None:\n+        \"\"\"Detect a qutebrowser version change.\"\"\"\n         old_qutebrowser_version = self['general'].get('version', None)\n         if old_qutebrowser_version is None:\n-            # https://github.com/python/typeshed/issues/2093\n-            return  # type: ignore[unreachable]\n+            return\n \n         try:\n             old_version = utils.VersionNumber.parse(old_qutebrowser_version)\n@@ -163,6 +178,46 @@ class StateConfig(configparser.ConfigParser):\n         else:\n             self.qutebrowser_version_changed = VersionChange.major\n \n+    def _set_chromium_changed_attribute(self) -&gt; None:\n+        if not self._has_webengine():\n+            return\n+\n+        old_chromium_version_str = self['general'].get('chromium_version', None)\n+        if old_chromium_version_str in ['no', None]:\n+            old_qtwe_version = self['general'].get('qtwe_version', None)\n+            if old_qtwe_version in ['no', None]:\n+                return\n+\n+            try:\n+                old_chromium_version = version.WebEngineVersions.from_webengine(\n+                    old_qtwe_version, source='config').chromium_major\n+            except ValueError:\n+                log.init.warning(\n+                    f\"Unable to parse old QtWebEngine version {old_qtwe_version}\")\n+                return\n+        else:\n+            try:\n+                old_chromium_version = int(old_chromium_version_str)\n+            except ValueError:\n+                log.init.warning(\n+                    f\"Unable to parse old Chromium version {old_chromium_version_str}\")\n+                return\n+\n+        new_versions = version.qtwebengine_versions(avoid_init=True)\n+        new_chromium_version = new_versions.chromium_major\n+\n+        if old_chromium_version is None or new_chromium_version is None:\n+            return\n+\n+        if old_chromium_version &lt;= 87 and new_chromium_version &gt;= 90:  # Qt 5 -&gt; Qt 6\n+            self.chromium_version_changed = VersionChange.major\n+        elif old_chromium_version &gt; new_chromium_version:\n+            self.chromium_version_changed = VersionChange.downgrade\n+        elif old_chromium_version == new_chromium_version:\n+            self.chromium_version_changed = VersionChange.equal\n+        else:\n+            self.chromium_version_changed = VersionChange.minor\n+\n     def init_save_manager(self,\n                           save_manager: 'savemanager.SaveManager') -&gt; None:\n         \"\"\"Make sure the config gets saved properly.\n@@ -238,7 +293,7 @@ class YamlConfig(QObject):\n             f.write(textwrap.dedent(\"\"\"\n                 # If a config.py file exists, this file is ignored unless it's explicitly loaded\n                 # via config.load_autoconfig(). For more information, see:\n-                # https://github.com/qutebrowser/qutebrowser/blob/master/doc/help/configuring.asciidoc#loading-autoconfigyml\n+                # https://github.com/qutebrowser/qutebrowser/blob/main/doc/help/configuring.asciidoc#loading-autoconfigyml\n                 # DO NOT edit this file by hand, qutebrowser will overwrite it.\n                 # Instead, create a config.py - see :help for details.\n \ndiff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py\nindex 03453dded..a08ddb619 100644\n--- a/qutebrowser/config/configinit.py\n+++ b/qutebrowser/config/configinit.py\n@@ -1,27 +1,13 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Initialization of the configuration.\"\"\"\n \n import argparse\n import os.path\n import sys\n+from typing import Optional\n \n from qutebrowser.qt.widgets import QMessageBox\n \n@@ -34,7 +20,7 @@ from qutebrowser.misc import msgbox, objects, savemanager\n \n \n # Error which happened during init, so we can show a message box.\n-_init_errors = None\n+_init_errors: Optional[configexc.ConfigFileErrors] = None\n \n \n def early_init(args: argparse.Namespace) -&gt; None:\ndiff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py\nindex fc134559b..e789437d3 100644\n--- a/qutebrowser/config/configtypes.py\n+++ b/qutebrowser/config/configtypes.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Types for options in qutebrowser's configuration.\n \n@@ -1106,7 +1091,7 @@ class QtColor(BaseType):\n         mult = 359.0 if kind == 'h' else 255.0\n         if val.endswith('%'):\n             val = val[:-1]\n-            mult = mult / 100\n+            mult /= 100\n \n         try:\n             return int(float(val) * mult)\n@@ -1167,7 +1152,7 @@ class QssColor(BaseType):\n     * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages)\n     * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)\n     * A gradient as explained in\n-      https://doc.qt.io/qt-5/stylesheet-reference.html#list-of-property-types[the Qt documentation]\n+      https://doc.qt.io/qt-6/stylesheet-reference.html#list-of-property-types[the Qt documentation]\n       under ``Gradient''\n     \"\"\"\n \ndiff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py\nindex a8a9ba246..fda9552dd 100644\n--- a/qutebrowser/config/configutils.py\n+++ b/qutebrowser/config/configutils.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utilities and data structures used by various config code.\"\"\"\n \n@@ -317,13 +301,13 @@ class FontFamilies:\n \n         They yield different results depending on the OS:\n \n-                   QFont.StyleHint.Monospace  | QFont.StyleHint.TypeWriter    | QFontDatabase\n-                   ------------------------------------------------------\n-        Windows:   Courier New      | Courier New         | Courier New\n-        Linux:     DejaVu Sans Mono | DejaVu Sans Mono    | monospace\n-        macOS:     Menlo            | American Typewriter | Monaco\n+                QFont.StyleHint.Monospace  | QFont.StyleHint.TypeWriter | QFontDatabase\n+                -----------------------------------------------------------------------\n+        Win:    Courier New                | Courier New                | Courier New\n+        Linux:  DejaVu Sans Mono           | DejaVu Sans Mono           | monospace\n+        macOS:  Menlo                      | American Typewriter        | Monaco\n \n-        Test script: https://p.cmpl.cc/d4dfe573\n+        Test script: https://p.cmpl.cc/076835c4\n \n         On Linux, it seems like both actually resolve to the same font.\n \ndiff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py\nindex fa529ffac..3a648524e 100644\n--- a/qutebrowser/config/qtargs.py\n+++ b/qutebrowser/config/qtargs.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Get arguments to pass to Qt.\"\"\"\n \n@@ -23,8 +8,9 @@ import os\n import sys\n import argparse\n import pathlib\n-from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple\n+from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union, Callable\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import QLocale\n \n from qutebrowser.config import config\n@@ -75,10 +61,15 @@ def qt_args(namespace: argparse.Namespace) -&gt; List[str]:\n         log.init.debug(\"QtWebEngine requested, but unavailable...\")\n         return argv\n \n+    versions = version.qtwebengine_versions(avoid_init=True)\n+    if versions.webengine &gt;= utils.VersionNumber(6, 4):\n+        # https://codereview.qt-project.org/c/qt/qtwebengine/+/376704\n+        argv.insert(1, \"--webEngineArgs\")\n+\n     special_prefixes = (_ENABLE_FEATURES, _DISABLE_FEATURES, _BLINK_SETTINGS)\n     special_flags = [flag for flag in argv if flag.startswith(special_prefixes)]\n     argv = [flag for flag in argv if not flag.startswith(special_prefixes)]\n-    argv += list(_qtwebengine_args(namespace, special_flags))\n+    argv += list(_qtwebengine_args(versions, namespace, special_flags))\n \n     return argv\n \n@@ -93,6 +84,8 @@ def _qtwebengine_features(\n         versions: The WebEngineVersions to get flags for.\n         special_flags: Existing flags passed via the commandline.\n     \"\"\"\n+    assert versions.chromium_major is not None\n+\n     enabled_features = []\n     disabled_features = []\n \n@@ -108,7 +101,7 @@ def _qtwebengine_features(\n         else:\n             raise utils.Unreachable(flag)\n \n-    if versions.webengine &gt;= utils.VersionNumber(5, 15, 1) and utils.is_linux:\n+    if utils.is_linux:\n         # Enable WebRTC PipeWire for screen capturing on Wayland.\n         #\n         # This is disabled in Chromium by default because of the \"dialog hell\":\n@@ -118,7 +111,7 @@ def _qtwebengine_features(\n         # However, we don't have Chromium's confirmation dialog in qutebrowser,\n         # so we should only get qutebrowser's permission dialog.\n         #\n-        # In theory this would be supported with Qt 5.13 already, but\n+        # In theory this would be supported with Qt 5.15.0 already, but\n         # QtWebEngine only started picking up PipeWire correctly with Qt\n         # 5.15.1.\n         #\n@@ -143,8 +136,8 @@ def _qtwebengine_features(\n         if config.val.scrolling.bar == 'overlay':\n             enabled_features.append('OverlayScrollbar')\n \n-    if (versions.webengine &gt;= utils.VersionNumber(5, 14) and\n-            config.val.content.headers.referer == 'same-domain'):\n+    if (config.val.content.headers.referer == 'same-domain' and\n+            versions.chromium_major &lt; 89):\n         # Handling of reduced-referrer-granularity in Chromium 76+\n         # https://chromium-review.googlesource.com/c/chromium/src/+/1572699\n         #\n@@ -210,7 +203,7 @@ def _get_lang_override(\n     \"\"\"Get a --lang switch to override Qt's locale handling.\n \n     This is needed as a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91715\n-    There is no fix yet, but we assume it'll be fixed with QtWebEngine 5.15.4.\n+    Fixed with QtWebEngine 5.15.4.\n     \"\"\"\n     if not config.val.qt.workarounds.locale:\n         return None\n@@ -239,29 +232,15 @@ def _get_lang_override(\n \n \n def _qtwebengine_args(\n+        versions: version.WebEngineVersions,\n         namespace: argparse.Namespace,\n         special_flags: Sequence[str],\n ) -&gt; Iterator[str]:\n     \"\"\"Get the QtWebEngine arguments to use based on the config.\"\"\"\n-    versions = version.qtwebengine_versions(avoid_init=True)\n-\n-    qt_514_ver = utils.VersionNumber(5, 14)\n-    qt_515_ver = utils.VersionNumber(5, 15)\n-    if qt_514_ver &lt;= versions.webengine &lt; qt_515_ver:\n-        # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-82105\n-        yield '--disable-shared-workers'\n-\n-    # WORKAROUND equivalent to\n     # https://codereview.qt-project.org/c/qt/qtwebengine/+/256786\n-    # also see:\n     # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/265753\n-    if versions.webengine &gt;= utils.VersionNumber(5, 12, 3):\n-        if 'stack' in namespace.debug_flags:\n-            # Only actually available in Qt 5.12.5, but let's save another\n-            # check, as passing the option won't hurt.\n-            yield '--enable-in-process-stack-traces'\n-    elif 'stack' not in namespace.debug_flags:\n-        yield '--disable-in-process-stack-traces'\n+    if 'stack' in namespace.debug_flags:\n+        yield '--enable-in-process-stack-traces'\n \n     lang_override = _get_lang_override(\n         webengine_version=versions.webengine,\n@@ -297,82 +276,88 @@ def _qtwebengine_args(\n     yield from _qtwebengine_settings_args(versions)\n \n \n-def _qtwebengine_settings_args(versions: version.WebEngineVersions) -&gt; Iterator[str]:\n-    settings: Dict[str, Dict[Any, Optional[str]]] = {\n-        'qt.force_software_rendering': {\n-            'software-opengl': None,\n-            'qt-quick': None,\n-            'chromium': '--disable-gpu',\n-            'none': None,\n-        },\n-        'content.canvas_reading': {\n-            True: None,\n-            False: '--disable-reading-from-canvas',\n-        },\n-        'content.webrtc_ip_handling_policy': {\n-            'all-interfaces': None,\n-            'default-public-and-private-interfaces':\n-                '--force-webrtc-ip-handling-policy='\n-                'default_public_and_private_interfaces',\n-            'default-public-interface-only':\n-                '--force-webrtc-ip-handling-policy='\n-                'default_public_interface_only',\n-            'disable-non-proxied-udp':\n-                '--force-webrtc-ip-handling-policy='\n-                'disable_non_proxied_udp',\n-        },\n-        'qt.chromium.process_model': {\n-            'process-per-site-instance': None,\n-            'process-per-site': '--process-per-site',\n-            'single-process': '--single-process',\n-        },\n-        'qt.chromium.low_end_device_mode': {\n-            'auto': None,\n-            'always': '--enable-low-end-device-mode',\n-            'never': '--disable-low-end-device-mode',\n-        },\n-        'content.headers.referer': {\n-            'always': None,\n-        },\n-        'content.prefers_reduced_motion': {\n-            True: '--force-prefers-reduced-motion',\n-            False: None,\n-        },\n-        'qt.chromium.sandboxing': {\n-            'enable-all': None,\n-            'disable-seccomp-bpf': '--disable-seccomp-filter-sandbox',\n-            'disable-all': '--no-sandbox',\n-        }\n-    }\n-    qt_514_ver = utils.VersionNumber(5, 14)\n-\n-    if qt_514_ver &lt;= versions.webengine &lt; utils.VersionNumber(5, 15, 2):\n-        # In Qt 5.14 to 5.15.1, `--force-dark-mode` is used to set the\n-        # preferred colorscheme. In Qt 5.15.2, this is handled by a\n-        # blink-setting in browser/webengine/darkmode.py instead.\n-        settings['colors.webpage.preferred_color_scheme'] = {\n-            'dark': '--force-dark-mode',\n-            'light': None,\n-            'auto': None,\n-        }\n-\n-    referrer_setting = settings['content.headers.referer']\n-    if versions.webengine &gt;= qt_514_ver:\n-        # Starting with Qt 5.14, this is handled via --enable-features\n-        referrer_setting['same-domain'] = None\n-    else:\n-        referrer_setting['same-domain'] = '--reduced-referrer-granularity'\n+_SettingValueType = Union[\n+    str,\n+    Callable[\n+        [\n+            version.WebEngineVersions,\n+        ],\n+        Optional[str],\n+    ],\n+]\n+_WEBENGINE_SETTINGS: Dict[str, Dict[Any, Optional[_SettingValueType]]] = {\n+    'qt.force_software_rendering': {\n+        'software-opengl': None,\n+        'qt-quick': None,\n+        'chromium': '--disable-gpu',\n+        'none': None,\n+    },\n+    'content.canvas_reading': {\n+        True: None,\n+        # might be overridden in webenginesettings.py\n+        False: '--disable-reading-from-canvas',\n+    },\n+    'content.webrtc_ip_handling_policy': {\n+        'all-interfaces': None,\n+        'default-public-and-private-interfaces':\n+            '--force-webrtc-ip-handling-policy='\n+            'default_public_and_private_interfaces',\n+        'default-public-interface-only':\n+            '--force-webrtc-ip-handling-policy='\n+            'default_public_interface_only',\n+        'disable-non-proxied-udp':\n+            '--force-webrtc-ip-handling-policy='\n+            'disable_non_proxied_udp',\n+    },\n+    'content.javascript.legacy_touch_events': {\n+        'always': '--touch-events=enabled',\n+        'auto': '--touch-events=auto',\n+        'never': '--touch-events=disabled',\n+    },\n+    'qt.chromium.process_model': {\n+        'process-per-site-instance': None,\n+        'process-per-site': '--process-per-site',\n+        'single-process': '--single-process',\n+    },\n+    'qt.chromium.low_end_device_mode': {\n+        'auto': None,\n+        'always': '--enable-low-end-device-mode',\n+        'never': '--disable-low-end-device-mode',\n+    },\n+    'content.prefers_reduced_motion': {\n+        True: '--force-prefers-reduced-motion',\n+        False: None,\n+    },\n+    'qt.chromium.sandboxing': {\n+        'enable-all': None,\n+        'disable-seccomp-bpf': '--disable-seccomp-filter-sandbox',\n+        'disable-all': '--no-sandbox',\n+    },\n+    'qt.chromium.experimental_web_platform_features': {\n+        'always': '--enable-experimental-web-platform-features',\n+        'never': None,\n+        'auto':\n+            '--enable-experimental-web-platform-features' if machinery.IS_QT5 else None,\n+    },\n+    'qt.workarounds.disable_accelerated_2d_canvas': {\n+        'always': '--disable-accelerated-2d-canvas',\n+        'never': None,\n+        'auto': lambda _versions: '--disable-accelerated-2d-canvas' if machinery.IS_QT6 else None,\n+    },\n+}\n \n-    # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-60203\n-    can_override_referer = (\n-        versions.webengine &gt;= utils.VersionNumber(5, 12, 4) and\n-        versions.webengine != utils.VersionNumber(5, 13)\n-    )\n-    referrer_setting['never'] = None if can_override_referer else '--no-referrers'\n \n-    for setting, args in sorted(settings.items()):\n+def _qtwebengine_settings_args(versions: version.WebEngineVersions) -&gt; Iterator[str]:\n+    for setting, args in sorted(_WEBENGINE_SETTINGS.items()):\n         arg = args[config.instance.get(setting)]\n-        if arg is not None:\n+        if callable(arg):\n+            result = arg(versions)\n+            if result is not None:\n+                assert isinstance(\n+                    result, str\n+                ), f\"qt.settings feature detection returned an invalid type: {type(result)} for {setting}\"\n+                yield result\n+        elif arg is not None:\n             yield arg\n \n \n@@ -411,10 +396,7 @@ def init_envvars() -&gt; None:\n         os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'\n \n     if config.val.qt.highdpi:\n-        env_var = ('QT_ENABLE_HIGHDPI_SCALING'\n-                   if qtutils.version_check('5.14', compiled=False)\n-                   else 'QT_AUTO_SCREEN_SCALE_FACTOR')\n-        os.environ[env_var] = '1'\n+        os.environ['QT_ENABLE_HIGHDPI_SCALING'] = '1'\n \n     for var, val in config.val.qt.environ.items():\n         if val is None and var in os.environ:\ndiff --git a/qutebrowser/config/stylesheet.py b/qutebrowser/config/stylesheet.py\nindex 8ad873ab5..d9032e2a9 100644\n--- a/qutebrowser/config/stylesheet.py\n+++ b/qutebrowser/config/stylesheet.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2019-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Handling of Qt qss stylesheets.\"\"\"\n \n@@ -48,7 +33,7 @@ def set_register(obj: QWidget,\n \n \n @debugcachestats.register()\n-@functools.lru_cache()\n+@functools.lru_cache\n def _render_stylesheet(stylesheet: str) -&gt; str:\n     \"\"\"Render the given stylesheet jinja template.\"\"\"\n     with jinja.environment.no_autoescape():\ndiff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py\nindex 7f59367cb..7824ae258 100644\n--- a/qutebrowser/config/websettings.py\n+++ b/qutebrowser/config/websettings.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Bridge from QWeb(Engine)Settings to our own settings.\"\"\"\n \n@@ -203,7 +188,7 @@ class AbstractSettings:\n \n \n @debugcachestats.register(name='user agent cache')\n-@functools.lru_cache()\n+@functools.lru_cache\n def _format_user_agent(template: str, backend: usertypes.Backend) -&gt; str:\n     if backend == usertypes.Backend.QtWebEngine:\n         from qutebrowser.browser.webengine import webenginesettings\ndiff --git a/qutebrowser/extensions/interceptors.py b/qutebrowser/extensions/interceptors.py\nindex ae537492e..8aaa9b28c 100644\n--- a/qutebrowser/extensions/interceptors.py\n+++ b/qutebrowser/extensions/interceptors.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Infrastructure for intercepting requests.\"\"\"\n \n@@ -30,7 +15,7 @@ class ResourceType(enum.Enum):\n     \"\"\"Possible request types that can be received.\n \n     Currently corresponds to the QWebEngineUrlRequestInfo Enum:\n-    https://doc.qt.io/qt-5/qwebengineurlrequestinfo.html#ResourceType-enum\n+    https://doc.qt.io/qt-6/qwebengineurlrequestinfo.html#ResourceType-enum\n     \"\"\"\n \n     main_frame = 0\n@@ -54,6 +39,7 @@ class ResourceType(enum.Enum):\n     # 18 is \"preload\", deprecated in Chromium\n     preload_main_frame = 19\n     preload_sub_frame = 20\n+    websocket = 254\n     unknown = 255\n \n \ndiff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py\nindex 073acd44c..ff9974d9d 100644\n--- a/qutebrowser/extensions/loader.py\n+++ b/qutebrowser/extensions/loader.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Loader for qutebrowser extensions.\"\"\"\n \n@@ -36,7 +21,7 @@ from qutebrowser.misc import objects\n \n \n # ModuleInfo objects for all loaded plugins\n-_module_infos = []\n+_module_infos: List[\"ModuleInfo\"] = []\n \n InitHookType = Callable[['InitContext'], None]\n ConfigChangedHookType = Callable[[], None]\ndiff --git a/qutebrowser/html/license.html b/qutebrowser/html/license.html\nindex 3cbc6c74b..3b2a1a9d3 100644\n--- a/qutebrowser/html/license.html\n+++ b/qutebrowser/html/license.html\n@@ -1,3 +1,7 @@\n+\n+\n \n \ndiff --git a/qutebrowser/html/version.html b/qutebrowser/html/version.html\nindex 27fa4eb4c..666414b26 100644\n--- a/qutebrowser/html/version.html\n+++ b/qutebrowser/html/version.html\n@@ -1,3 +1,7 @@\n+\n+\n {% extends \"base.html\" %}\n \n {% block script %}\n@@ -15,8 +19,8 @@ html { margin-left: 10px; }\n {% block content %}\n {{ super() }}\n \nVersion info\n-\n{{ version }}\n Yank pastebin URL for version info\n+\n{{ version }}\n \n \nCopyright info\n \n{{ copyright }}\ndiff --git a/qutebrowser/html/warning-qt5.html b/qutebrowser/html/warning-qt5.html\nnew file mode 100644\nindex 000000000..17af2f72c\n--- /dev/null\n+++ b/qutebrowser/html/warning-qt5.html\n@@ -0,0 +1,28 @@\n+{% extends \"styled.html\" %}\n+\n+{% block content %}\n+\n{{ title }}\n+Note this warning will only appear once. Use :open\n+qute://warning/qt5 to show it again at a later time.\n+\n+\n\n+    qutebrowser now supports Qt 6.\n+\n+\n\n+    However, in your environment, Qt 6 is not installed. Thus, qutebrowser is still using Qt 5 instead.\n+\n+    Qt 5.15 based on a very old Chromium version (83 or 87, from mid/late 2020).\n+\n+{% if is_venv %}\n+\n\n+    You are using a virtualenv. If you want to use Qt 6, you need to create a new\n+    virtualenv with PyQt6 installed.\n+\n+    If using mkvenv.py, rerun the script to create a\n+    new virtualenv with Qt 6.\n+\n+{% endif %}\n+\n\n+    Python installation prefix: {{ prefix }}\n+\n+{% endblock %}\ndiff --git a/qutebrowser/html/warning-sessions.html b/qutebrowser/html/warning-sessions.html\nindex 422b409a9..9c5d459cb 100644\n--- a/qutebrowser/html/warning-sessions.html\n+++ b/qutebrowser/html/warning-sessions.html\n@@ -5,7 +5,7 @@\n Note this warning will only appear once. Use :open\n qute://warning/sessions to show it again at a later time.\n \n-\nYou're using qutebrowser with Qt 5.15. While this is the recommended Qt version to use (due to QtWebEngine security updates), qutebrowser only provides partial support for session files.\n+\nYou're using qutebrowser with Qt &gt;= 5.15. While this is the recommended Qt version to use (due to QtWebEngine security updates), qutebrowser only provides partial support for session files.\n \n \nSince Qt doesn't provide an API to load the history of a tab, qutebrowser relies on a reverse-engineered binary serialization format to load tab history from session files. With Qt 5.15, unfortunately that format changed (due to the underlying Chromium upgrade), in a way which makes it impossible for qutebrowser to load tab history from existing session data.\n \ndiff --git a/qutebrowser/html/warning-webkit.html b/qutebrowser/html/warning-webkit.html\nindex f5cf9bf01..e9612ce37 100644\n--- a/qutebrowser/html/warning-webkit.html\n+++ b/qutebrowser/html/warning-webkit.html\n@@ -37,8 +37,7 @@ class=\"mono\"&gt;content.cookies.accept setting works on QtWebEngine.\n class=\"mono\"&gt;qt.force_software_rendering setting added in v1.4.0 should\n hopefully help.\n \n-\nMissing support for notifications: With qutebrowser v1.7.0, initial\n-notification support was added for Qt 5.13.0.\n+\nMissing support for notifications: Supported since qutebrowser v1.7.0.\n \n \nResource usage: qutebrowser v1.5.0 added the qt.chromium.process_model and \n \n-\nNouveau graphic driver: You can use QtWebEngine with software\n-rendering. With Qt 5.13 (~May 2019) it might be possible to run with Nouveau\n-without software rendering.\n+\nNouveau graphic driver: Should be handled properly in current versions\n+of QtWebEngine.\n \n \nWayland: It's possible to use QtWebEngine with XWayland. With Qt\n 5.11.2 or newer, qutebrowser also runs natively with Wayland.\ndiff --git a/qutebrowser/javascript/.eslintrc.yaml b/qutebrowser/javascript/.eslintrc.yaml\nindex 939500aa3..566304c27 100644\n--- a/qutebrowser/javascript/.eslintrc.yaml\n+++ b/qutebrowser/javascript/.eslintrc.yaml\n@@ -29,7 +29,7 @@ rules:\n     init-declarations: \"off\"\n     no-plusplus: \"off\"\n     no-extra-parens: \"off\"\n-    id-length: [\"error\", {\"exceptions\": [\"i\", \"k\", \"v\", \"x\", \"y\"]}]\n+    id-length: [\"error\", {\"exceptions\": [\"i\", \"n\", \"k\", \"v\", \"x\", \"y\"]}]\n     object-shorthand: \"off\"\n     max-statements: [\"error\", {\"max\": 40}]\n     quotes: [\"error\", \"double\", {\"avoidEscape\": true}]\n@@ -61,3 +61,4 @@ rules:\n     prefer-named-capture-group: \"off\"\n     function-call-argument-newline: \"off\"\n     no-negated-condition: \"off\"\n+    no-console: \"off\"\ndiff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js\nindex 7c157142c..4aeefcdb9 100644\n--- a/qutebrowser/javascript/caret.js\n+++ b/qutebrowser/javascript/caret.js\n@@ -1,51 +1,17 @@\n /* eslint-disable max-len, max-statements, complexity,\n default-case */\n \n-// Copyright 2014 The Chromium Authors. All rights reserved.\n-//\n-// Redistribution and use in source and binary forms, with or without\n-// modification, are permitted provided that the following conditions are\n-// met:\n-//\n-//    * Redistributions of source code must retain the above copyright\n-// notice, this list of conditions and the following disclaimer.\n-//    * Redistributions in binary form must reproduce the above\n-// copyright notice, this list of conditions and the following disclaimer\n-// in the documentation and/or other materials provided with the\n-// distribution.\n-//    * Neither the name of Google Inc. nor the names of its\n-// contributors may be used to endorse or promote products derived from\n-// this software without specific prior written permission.\n-//\n-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n-// \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n+/* Copyright 2014 The Chromium Authors\n+ * Use of this source code is governed by a BSD-style license that can be\n+ * found in the LICENSE file.\n+ * https://source.chromium.org/chromium/chromium/src/+/main:LICENSE\n+ */\n+\n \n /**\n- * Copyright 2018-2021 Florian Bruhin (The Compiler) \n- *\n- * This file is part of qutebrowser.\n- *\n- * qutebrowser is free software: you can redistribute it and/or modify\n- * it under the terms of the GNU General Public License as published by\n- * the Free Software Foundation, either version 3 of the License, or\n- * (at your option) any later version.\n+ * SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n  *\n- * qutebrowser is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n- * GNU General Public License for more details.\n- *\n- * You should have received a copy of the GNU General Public License\n- * along with qutebrowser.  If not, see .\n+ * SPDX-License-Identifier: GPL-3.0-or-later\n  */\n \n /**\n@@ -773,6 +739,12 @@ window._qutebrowser.caret = (function() {\n      */\n     CaretBrowsing.isWindows = null;\n \n+    /**\n+     * Whether we should log debug outputs.\n+     * @type {boolean}\n+     */\n+    CaretBrowsing.isDebug = null;\n+\n     /**\n      * The id returned by window.setInterval for our stopAnimation function, so\n      * we can cancel it when we call stopAnimation again.\n@@ -1184,6 +1156,8 @@ window._qutebrowser.caret = (function() {\n             action = \"extend\";\n         }\n \n+        CaretBrowsing.debug(`(move) ${action} ${count} ${granularity} ${direction}, selection ${CaretBrowsing.selectionState}`);\n+\n         for (let i = 0; i &lt; count; i++) {\n             if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) {\n                 CaretBrowsing.updateLineSelection(direction, granularity);\n@@ -1214,6 +1188,8 @@ window._qutebrowser.caret = (function() {\n         if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) {\n             action = \"extend\";\n         }\n+        CaretBrowsing.debug(`(moveToBlock) ${action} paragraph ${paragraph}, boundary ${boundary}, count ${count}, selection ${CaretBrowsing.selectionState}`);\n+\n         for (let i = 0; i &lt; count; i++) {\n             window.\n                 getSelection().\n@@ -1230,6 +1206,7 @@ window._qutebrowser.caret = (function() {\n     };\n \n     CaretBrowsing.toggle = function(value) {\n+        CaretBrowsing.debug(`(toggle) enabled ${CaretBrowsing.isEnabled}, force ${CaretBrowsing.forceEnabled}`);\n         if (CaretBrowsing.forceEnabled) {\n             CaretBrowsing.recreateCaretElement();\n             return;\n@@ -1265,6 +1242,7 @@ window._qutebrowser.caret = (function() {\n      * is enabled and whether this window / iframe has focus.\n      */\n     CaretBrowsing.updateIsCaretVisible = function() {\n+        CaretBrowsing.debug(`(updateIsCaretVisible) isEnabled ${CaretBrowsing.isEnabled}, isWindowFocused ${CaretBrowsing.isWindowFocused}, isCaretVisible ${CaretBrowsing.isCaretVisible}, caretElement ${CaretBrowsing.caretElement}`);\n         CaretBrowsing.isCaretVisible =\n             (CaretBrowsing.isEnabled &amp;&amp; CaretBrowsing.isWindowFocused);\n         if (CaretBrowsing.isCaretVisible &amp;&amp; !CaretBrowsing.caretElement) {\n@@ -1308,6 +1286,12 @@ window._qutebrowser.caret = (function() {\n         }\n     };\n \n+    CaretBrowsing.debug = (text) =&gt; {\n+        if (CaretBrowsing.isDebug) {\n+            console.debug(`caret: ${text}`);\n+        }\n+    }\n+\n     CaretBrowsing.init = function() {\n         CaretBrowsing.isWindowFocused = document.hasFocus();\n \n@@ -1347,6 +1331,7 @@ window._qutebrowser.caret = (function() {\n \n     funcs.setFlags = (flags) =&gt; {\n         CaretBrowsing.isWindows = flags.includes(\"windows\");\n+        CaretBrowsing.isDebug = flags.includes(\"debug\");\n     };\n \n     funcs.disableCaret = () =&gt; {\ndiff --git a/qutebrowser/javascript/history.js b/qutebrowser/javascript/history.js\nindex a50e42d6b..428adae33 100644\n--- a/qutebrowser/javascript/history.js\n+++ b/qutebrowser/javascript/history.js\n@@ -1,22 +1,7 @@\n-/**\n- * Copyright 2017-2021 Florian Bruhin (The-Compiler) \n- * Copyright 2017 Imran Sobir\n- *\n- * This file is part of qutebrowser.\n- *\n- * qutebrowser is free software: you can redistribute it and/or modify\n- * it under the terms of the GNU General Public License as published by\n- * the Free Software Foundation, either version 3 of the License, or\n- * (at your option) any later version.\n- *\n- * qutebrowser is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n- * GNU General Public License for more details.\n- *\n- * You should have received a copy of the GNU General Public License\n- * along with qutebrowser.  If not, see .\n- */\n+// SPDX-FileCopyrightText: Imran Sobir\n+// SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n+//\n+// SPDX-License-Identifier: GPL-3.0-or-later\n \n \"use strict\";\n \ndiff --git a/qutebrowser/javascript/position_caret.js b/qutebrowser/javascript/position_caret.js\nindex 7b53008fa..ba66c6d88 100644\n--- a/qutebrowser/javascript/position_caret.js\n+++ b/qutebrowser/javascript/position_caret.js\n@@ -1,22 +1,7 @@\n-/**\n-* Copyright 2015-2021 Florian Bruhin (The Compiler) \n-* Copyright 2015 Artur Shaik \n-*\n-* This file is part of qutebrowser.\n-*\n-* qutebrowser is free software: you can redistribute it and/or modify\n-* it under the terms of the GNU General Public License as published by\n-* the Free Software Foundation, either version 3 of the License, or\n-* (at your option) any later version.\n-*\n-* qutebrowser is distributed in the hope that it will be useful,\n-* but WITHOUT ANY WARRANTY; without even the implied warranty of\n-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-* GNU General Public License for more details.\n-*\n-* You should have received a copy of the GNU General Public License\n-* along with qutebrowser.  If not, see .\n-*/\n+// SPDX-FileCopyrightText: Artur Shaik \n+// SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+//\n+// SPDX-License-Identifier: GPL-3.0-or-later\n \n /**\n  * Snippet to position caret at top of the page when caret mode is enabled.\ndiff --git a/qutebrowser/javascript/quirks/array_at.user.js b/qutebrowser/javascript/quirks/array_at.user.js\nnew file mode 100644\nindex 000000000..0badebd77\n--- /dev/null\n+++ b/qutebrowser/javascript/quirks/array_at.user.js\n@@ -0,0 +1,58 @@\n+// Based on: https://github.com/tc39/proposal-relative-indexing-method#polyfill\n+\n+/*\n+Copyright (c) 2020 Tab Atkins Jr.\n+\n+Permission is hereby granted, free of charge, to any person obtaining a copy\n+of this software and associated documentation files (the \"Software\"), to deal\n+in the Software without restriction, including without limitation the rights\n+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+copies of the Software, and to permit persons to whom the Software is\n+furnished to do so, subject to the following conditions:\n+\n+The above copyright notice and this permission notice shall be included in all\n+copies or substantial portions of the Software.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n+SOFTWARE.\n+*/\n+\n+/* eslint-disable no-invalid-this */\n+\n+\"use strict\";\n+\n+(function() {\n+    function at(idx) {\n+        // ToInteger() abstract op\n+        let n = Math.trunc(idx) || 0;\n+        // Allow negative indexing from the end\n+        if (n &lt; 0) {\n+            n += this.length;\n+        }\n+        // OOB access is guaranteed to return undefined\n+        if (n &lt; 0 || n &gt;= this.length) {\n+            return undefined;\n+        }\n+        // Otherwise, this is just normal property access\n+        return this[n];\n+    }\n+\n+    const TypedArray = Reflect.getPrototypeOf(Int8Array);\n+    for (const type of [Array, String, TypedArray]) {\n+        Object.defineProperty(\n+            type.prototype,\n+            \"at\",\n+            {\n+                \"value\": at,\n+                \"writable\": true,\n+                \"enumerable\": false,\n+                \"configurable\": true,\n+            }\n+        );\n+    }\n+})();\ndiff --git a/qutebrowser/javascript/quirks/globalthis.user.js b/qutebrowser/javascript/quirks/globalthis.user.js\ndeleted file mode 100644\nindex b87a956e5..000000000\n--- a/qutebrowser/javascript/quirks/globalthis.user.js\n+++ /dev/null\n@@ -1,12 +0,0 @@\n-// ==UserScript==\n-// @include https://www.reddit.com/*\n-// @include https://open.spotify.com/*\n-// @include https://*.stackexchange.com/*\n-// @include https://stackoverflow.com/*\n-// @include https://test.qutebrowser.org/*\n-// ==/UserScript==\n-\n-// Polyfill for a failing globalThis with older Qt versions.\n-\n-\"use strict\";\n-window.globalThis = window;\ndiff --git a/qutebrowser/javascript/quirks/object_fromentries.user.js b/qutebrowser/javascript/quirks/object_fromentries.user.js\ndeleted file mode 100644\nindex 6f6ad8b31..000000000\n--- a/qutebrowser/javascript/quirks/object_fromentries.user.js\n+++ /dev/null\n@@ -1,46 +0,0 @@\n-// Based on: https://gitlab.com/moongoal/js-polyfill-object.fromentries/-/tree/master\n-\n-/*\n-    Copyright 2018  Alfredo Mungo \n-\n-    Permission is hereby granted, free of charge, to any person obtaining a copy\n-    of this software and associated documentation files (the \"Software\"), to\n-    deal in the Software without restriction, including without limitation the\n-    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n-    sell copies of the Software, and to permit persons to whom the Software is\n-    furnished to do so, subject to the following conditions:\n-\n-    The above copyright notice and this permission notice shall be included in\n-    all copies or substantial portions of the Software.\n-\n-    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n-    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n-    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n-    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n-    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n-    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n-    IN THE SOFTWARE.\n-*/\n-\n-\"use strict\";\n-\n-if (!Object.fromEntries) {\n-    Object.defineProperty(Object, \"fromEntries\", {\n-        value(entries) {\n-            if (!entries || !entries[Symbol.iterator]) {\n-                throw new Error(\n-                    \"Object.fromEntries() requires a single iterable argument\");\n-            }\n-\n-            const obj = {};\n-\n-            Object.keys(entries).forEach((key) =&gt; {\n-                const [k, v] = entries[key];\n-                obj[k] = v;\n-            });\n-\n-            return obj;\n-        },\n-    });\n-}\n-\ndiff --git a/qutebrowser/javascript/scroll.js b/qutebrowser/javascript/scroll.js\nindex 8bb904c56..39b1f55db 100644\n--- a/qutebrowser/javascript/scroll.js\n+++ b/qutebrowser/javascript/scroll.js\n@@ -1,21 +1,6 @@\n-/**\n- * Copyright 2016-2021 Florian Bruhin (The Compiler) \n- *\n- * This file is part of qutebrowser.\n- *\n- * qutebrowser is free software: you can redistribute it and/or modify\n- * it under the terms of the GNU General Public License as published by\n- * the Free Software Foundation, either version 3 of the License, or\n- * (at your option) any later version.\n- *\n- * qutebrowser is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n- * GNU General Public License for more details.\n- *\n- * You should have received a copy of the GNU General Public License\n- * along with qutebrowser.  If not, see .\n- */\n+// SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+//\n+// SPDX-License-Identifier: GPL-3.0-or-later\n \n \"use strict\";\n \ndiff --git a/qutebrowser/javascript/stylesheet.js b/qutebrowser/javascript/stylesheet.js\nindex 21a62b25d..bf818356c 100644\n--- a/qutebrowser/javascript/stylesheet.js\n+++ b/qutebrowser/javascript/stylesheet.js\n@@ -1,22 +1,7 @@\n-/**\n- * Copyright 2017-2021 Florian Bruhin (The Compiler) \n- * Copyright 2017 Ulrik de Muelenaere \n- *\n- * This file is part of qutebrowser.\n- *\n- * qutebrowser is free software: you can redistribute it and/or modify\n- * it under the terms of the GNU General Public License as published by\n- * the Free Software Foundation, either version 3 of the License, or\n- * (at your option) any later version.\n- *\n- * qutebrowser is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n- * GNU General Public License for more details.\n- *\n- * You should have received a copy of the GNU General Public License\n- * along with qutebrowser.  If not, see .\n- */\n+// SPDX-FileCopyrightText: Ulrik de Muelenaere \n+// SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+//\n+// SPDX-License-Identifier: GPL-3.0-or-later\n \n \"use strict\";\n \n@@ -132,11 +117,19 @@ window._qutebrowser.stylesheet = (function() {\n             css_content = css;\n         }\n         // Propagate the new CSS to all child frames.\n-        // FIXME:qtwebengine This does not work for cross-origin frames.\n         for (let i = 0; i &lt; window.frames.length; ++i) {\n             const frame = window.frames[i];\n-            if (frame._qutebrowser &amp;&amp; frame._qutebrowser.stylesheet) {\n-                frame._qutebrowser.stylesheet.set_css(css);\n+            try {\n+                if (frame._qutebrowser &amp;&amp; frame._qutebrowser.stylesheet) {\n+                    frame._qutebrowser.stylesheet.set_css(css);\n+                }\n+            } catch (exc) {\n+                if (exc instanceof DOMException &amp;&amp; exc.name === \"SecurityError\") {\n+                    // FIXME:qtwebengine This does not work for cross-origin frames.\n+                    console.log(`Failed to style frame: ${exc.message}`);\n+                } else {\n+                    throw exc;\n+                }\n             }\n         }\n     };\ndiff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js\nindex b4cef24bc..5ea84c497 100644\n--- a/qutebrowser/javascript/webelem.js\n+++ b/qutebrowser/javascript/webelem.js\n@@ -1,21 +1,6 @@\n-/**\n- * Copyright 2016-2021 Florian Bruhin (The Compiler) \n- *\n- * This file is part of qutebrowser.\n- *\n- * qutebrowser is free software: you can redistribute it and/or modify\n- * it under the terms of the GNU General Public License as published by\n- * the Free Software Foundation, either version 3 of the License, or\n- * (at your option) any later version.\n- *\n- * qutebrowser is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n- * GNU General Public License for more details.\n- *\n- * You should have received a copy of the GNU General Public License\n- * along with qutebrowser.  If not, see .\n- */\n+// SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+//\n+// SPDX-License-Identifier: GPL-3.0-or-later\n \n /**\n  * The connection for web elements between Python and Javascript works like\n@@ -197,8 +182,12 @@ window._qutebrowser.webelem = (function() {\n         try {\n             frame.document; // eslint-disable-line no-unused-expressions\n             return true;\n-        } catch (err) {\n-            return false;\n+        } catch (exc) {\n+            if (exc instanceof DOMException &amp;&amp; exc.name === \"SecurityError\") {\n+                // FIXME:qtwebengine This does not work for cross-origin frames.\n+                return false;\n+            }\n+            throw exc;\n         }\n     }\n \ndiff --git a/qutebrowser/keyinput/__init__.py b/qutebrowser/keyinput/__init__.py\nindex 4acc6d7c3..307e67788 100644\n--- a/qutebrowser/keyinput/__init__.py\n+++ b/qutebrowser/keyinput/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Modules related to keyboard input and mode handling.\"\"\"\ndiff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py\nindex 8c0fac075..df6b66f7f 100644\n--- a/qutebrowser/keyinput/basekeyparser.py\n+++ b/qutebrowser/keyinput/basekeyparser.py\n@@ -1,34 +1,20 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Base class for vim-like key sequence parser.\"\"\"\n \n import string\n import types\n import dataclasses\n+import traceback\n from typing import Mapping, MutableMapping, Optional, Sequence\n \n-from qutebrowser.qt.core import pyqtSignal, QObject, Qt\n+from qutebrowser.qt.core import QObject, pyqtSignal\n from qutebrowser.qt.gui import QKeySequence, QKeyEvent\n \n from qutebrowser.config import config\n-from qutebrowser.utils import usertypes, log, utils, debug\n+from qutebrowser.utils import log, usertypes, utils, message\n from qutebrowser.keyinput import keyutils\n \n \n@@ -204,7 +190,7 @@ class BaseKeyParser(QObject):\n                               passthrough=self.passthrough,\n                               supports_count=self._supports_count)\n \n-    def _debug_log(self, message: str) -&gt; None:\n+    def _debug_log(self, msg: str) -&gt; None:\n         \"\"\"Log a message to the debug log if logging is active.\n \n         Args:\n@@ -213,7 +199,7 @@ class BaseKeyParser(QObject):\n         if self._do_log:\n             prefix = '{} for mode {}: '.format(self.__class__.__name__,\n                                                self._mode.name)\n-            log.keyboard.debug(prefix + message)\n+            log.keyboard.debug(prefix + msg)\n \n     def _match_key(self, sequence: keyutils.KeySequence) -&gt; MatchResult:\n         \"\"\"Try to match a given keystring with any bound keychain.\n@@ -284,14 +270,17 @@ class BaseKeyParser(QObject):\n         Return:\n             A QKeySequence match.\n         \"\"\"\n-        key = Qt.Key(e.key())\n-        txt = str(keyutils.KeyInfo.from_event(e))\n-        self._debug_log(\n-            f\"Got key: {debug.qenum_key(Qt.Key, key)} / \"\n-            f\"modifiers: {debug.qflags_key(Qt.KeyboardModifier, e.modifiers())} / \"\n-            f\"text: '{txt}' / dry_run {dry_run}\")\n-\n-        if keyutils.is_modifier_key(key):\n+        try:\n+            info = keyutils.KeyInfo.from_event(e)\n+        except keyutils.InvalidKeyError as ex:\n+            # See https://github.com/qutebrowser/qutebrowser/issues/7047\n+            log.keyboard.debug(f\"Got invalid key: {ex}\")\n+            self.clear_keystring()\n+            return QKeySequence.SequenceMatch.NoMatch\n+\n+        self._debug_log(f\"Got key: {info!r} (dry_run {dry_run})\")\n+\n+        if info.is_modifier_key():\n             self._debug_log(\"Ignoring, only modifier\")\n             return QKeySequence.SequenceMatch.NoMatch\n \n@@ -318,17 +307,29 @@ class BaseKeyParser(QObject):\n             return result.match_type\n \n         self._sequence = result.sequence\n+        self._handle_result(info, result)\n+        return result.match_type\n \n+    def _handle_result(self, info: keyutils.KeyInfo, result: MatchResult) -&gt; None:\n+        \"\"\"Handle a final MatchResult from handle().\"\"\"\n         if result.match_type == QKeySequence.SequenceMatch.ExactMatch:\n             assert result.command is not None\n             self._debug_log(\"Definitive match for '{}'.\".format(\n                 result.sequence))\n-            count = int(self._count) if self._count else None\n+\n+            try:\n+                count = int(self._count) if self._count else None\n+            except ValueError as err:\n+                message.error(f\"Failed to parse count: {err}\",\n+                              stack=traceback.format_exc())\n+                self.clear_keystring()\n+                return\n+\n             self.clear_keystring()\n             self.execute(result.command, count)\n         elif result.match_type == QKeySequence.SequenceMatch.PartialMatch:\n             self._debug_log(\"No match for '{}' (added {})\".format(\n-                result.sequence, txt))\n+                result.sequence, info))\n             self.keystring_updated.emit(self._count + str(result.sequence))\n         elif result.match_type == QKeySequence.SequenceMatch.NoMatch:\n             self._debug_log(\"Giving up with '{}', no matches\".format(\n@@ -338,8 +339,6 @@ class BaseKeyParser(QObject):\n             raise utils.Unreachable(\"Invalid match value {!r}\".format(\n                 result.match_type))\n \n-        return result.match_type\n-\n     @config.change_filter('bindings')\n     def _on_config_changed(self) -&gt; None:\n         self._read_config()\ndiff --git a/qutebrowser/keyinput/eventfilter.py b/qutebrowser/keyinput/eventfilter.py\nindex 31ffcc7f9..a9e7e78aa 100644\n--- a/qutebrowser/keyinput/eventfilter.py\n+++ b/qutebrowser/keyinput/eventfilter.py\n@@ -1,32 +1,17 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Global Qt event filter which dispatches key events.\"\"\"\n \n-from typing import cast\n+from typing import cast, Optional\n \n-from qutebrowser.qt.core import pyqtSlot, QObject, QEvent\n+from qutebrowser.qt.core import pyqtSlot, QObject, QEvent, qVersion\n from qutebrowser.qt.gui import QKeyEvent, QWindow\n \n from qutebrowser.keyinput import modeman\n from qutebrowser.misc import quitter, objects\n-from qutebrowser.utils import objreg\n+from qutebrowser.utils import objreg, debug, log, qtutils\n \n \n class EventFilter(QObject):\n@@ -47,6 +32,7 @@ class EventFilter(QObject):\n             QEvent.Type.KeyRelease: self._handle_key_event,\n             QEvent.Type.ShortcutOverride: self._handle_key_event,\n         }\n+        self._log_qt_events = \"log-qt-events\" in objects.debug_flags\n \n     def install(self) -&gt; None:\n         objects.qapp.installEventFilter(self)\n@@ -76,7 +62,7 @@ class EventFilter(QObject):\n             # No window available yet, or not a MainWindow\n             return False\n \n-    def eventFilter(self, obj: QObject, event: QEvent) -&gt; bool:\n+    def eventFilter(self, obj: Optional[QObject], event: Optional[QEvent]) -&gt; bool:\n         \"\"\"Handle an event.\n \n         Args:\n@@ -86,20 +72,43 @@ class EventFilter(QObject):\n         Return:\n             True if the event should be filtered, False if it's passed through.\n         \"\"\"\n+        assert event is not None\n+        ev_type = event.type()\n+\n+        if self._log_qt_events:\n+            try:\n+                source = repr(obj)\n+            except AttributeError:  # might not be fully initialized yet\n+                source = type(obj).__name__\n+\n+            ev_type_str = debug.qenum_key(QEvent, ev_type)\n+            log.misc.debug(f\"{source} got event: {ev_type_str}\")\n+\n+        if (\n+            ev_type == QEvent.Type.DragEnter and\n+            qtutils.is_wayland() and\n+            qVersion() == \"6.5.2\"\n+        ):\n+            # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-115757\n+            # Fixed in Qt 6.5.3\n+            # Can't do this via self._handlers since handling it for QWindow\n+            # seems to be too late.\n+            log.mouse.warning(\"Ignoring drag event to prevent Qt crash\")\n+            event.ignore()\n+            return True\n+\n         if not isinstance(obj, QWindow):\n             # We already handled this same event at some point earlier, so\n             # we're not interested in it anymore.\n             return False\n \n-        typ = event.type()\n-\n-        if typ not in self._handlers:\n+        if ev_type not in self._handlers:\n             return False\n \n         if not self._activated:\n             return False\n \n-        handler = self._handlers[typ]\n+        handler = self._handlers[ev_type]\n         try:\n             return handler(cast(QKeyEvent, event))\n         except:\ndiff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py\nindex fca70f0a9..18730c74b 100644\n--- a/qutebrowser/keyinput/keyutils.py\n+++ b/qutebrowser/keyinput/keyutils.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Our own QKeySequence-like class and related utilities.\n \n@@ -24,26 +9,35 @@ comes to keys/modifiers. Many places (such as QKeyEvent::key()) don't actually\n return a Qt::Key, they return an int.\n \n To make things worse, when talking about a \"key\", sometimes Qt means a Qt::Key\n-member. However, sometimes it means a Qt::Key member ORed with\n-Qt.KeyboardModifiers...\n+member. However, sometimes it means a Qt::Key member ORed with a\n+Qt.KeyboardModifier...\n \n Because of that, _assert_plain_key() and _assert_plain_modifier() make sure we\n handle what we actually think we do.\n \"\"\"\n \n-import enum\n import itertools\n import dataclasses\n-from typing import cast, overload, Iterable, Iterator, List, Mapping, Optional, Union\n+from typing import Iterator, Iterable, List, Mapping, Optional, Union, overload, cast\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import Qt, QEvent\n from qutebrowser.qt.gui import QKeySequence, QKeyEvent\n-try:\n+if machinery.IS_QT6:\n     from qutebrowser.qt.core import QKeyCombination\n-except ImportError:\n-    QKeyCombination = None  # Qt 6 only\n+else:\n+    QKeyCombination: None = None  # QKeyCombination was added in Qt 6\n+\n+from qutebrowser.utils import utils, qtutils, debug\n \n-from qutebrowser.utils import utils\n+\n+class InvalidKeyError(Exception):\n+\n+    \"\"\"Raised when a key can't be represented by PyQt.\n+\n+    WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044607.html\n+    Should be fixed in PyQt 6.3.1 (or 6.4.0?).\n+    \"\"\"\n \n \n # Map Qt::Key values to their Qt::KeyboardModifier value.\n@@ -56,9 +50,19 @@ _MODIFIER_MAP = {\n     Qt.Key.Key_Mode_switch: Qt.KeyboardModifier.GroupSwitchModifier,\n }\n \n-_NIL_KEY = 0\n+try:\n+    _NIL_KEY: Union[Qt.Key, int] = Qt.Key(0)\n+except ValueError:\n+    # WORKAROUND for\n+    # https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044607.html\n+    _NIL_KEY = 0\n \n-_ModifierType = Qt.KeyboardModifier\n+if machinery.IS_QT6:\n+    _KeyInfoType = QKeyCombination\n+    _ModifierType = Qt.KeyboardModifier\n+else:\n+    _KeyInfoType = int\n+    _ModifierType = Union[Qt.KeyboardModifiers, Qt.KeyboardModifier]\n \n \n _SPECIAL_NAMES = {\n@@ -79,11 +83,12 @@ _SPECIAL_NAMES = {\n     Qt.Key.Key_Control: 'Control',\n     Qt.Key.Key_Meta: 'Meta',\n     Qt.Key.Key_Alt: 'Alt',\n-\n     Qt.Key.Key_AltGr: 'AltGr',\n+\n     Qt.Key.Key_Multi_key: 'Multi key',\n     Qt.Key.Key_SingleCandidate: 'Single Candidate',\n     Qt.Key.Key_Mode_switch: 'Mode switch',\n+\n     Qt.Key.Key_Dead_Grave: '`',\n     Qt.Key.Key_Dead_Acute: '\u00b4',\n     Qt.Key.Key_Dead_Circumflex: '^',\n@@ -103,7 +108,6 @@ _SPECIAL_NAMES = {\n     Qt.Key.Key_Dead_Belowdot: 'Belowdot',\n     Qt.Key.Key_Dead_Hook: 'Hook',\n     Qt.Key.Key_Dead_Horn: 'Horn',\n-\n     Qt.Key.Key_Dead_Stroke: '\\u0335',  # '\u0335'\n     Qt.Key.Key_Dead_Abovecomma: '\\u0313',  # '\u0313'\n     Qt.Key.Key_Dead_Abovereversedcomma: '\\u0314',  # '\u0314'\n@@ -135,13 +139,6 @@ _SPECIAL_NAMES = {\n     Qt.Key.Key_Dead_Belowverticalline: '\\u0329',\n     Qt.Key.Key_Dead_Longsolidusoverlay: '\\u0338',  # '\u0338'\n \n-    Qt.Key.Key_Memo: 'Memo',\n-    Qt.Key.Key_ToDoList: 'To Do List',\n-    Qt.Key.Key_Calendar: 'Calendar',\n-    Qt.Key.Key_ContrastAdjust: 'Contrast Adjust',\n-    Qt.Key.Key_LaunchG: 'Launch (G)',\n-    Qt.Key.Key_LaunchH: 'Launch (H)',\n-\n     Qt.Key.Key_MediaLast: 'Media Last',\n \n     Qt.Key.Key_unknown: 'Unknown',\n@@ -153,23 +150,18 @@ _SPECIAL_NAMES = {\n }\n \n \n-def _extract_enum_val(val: Union[int, enum.Enum]) -&gt; int:\n-    \"\"\"Extract an int value from a Qt enum value.\"\"\"\n-    if isinstance(val, enum.Enum):\n-        return val.value\n-    return int(val)\n-\n-\n def _assert_plain_key(key: Qt.Key) -&gt; None:\n-    \"\"\"Make sure this is a key without KeyboardModifiers mixed in.\"\"\"\n-    mask = _extract_enum_val(Qt.KeyboardModifier.KeyboardModifierMask)\n-    assert not int(key) &amp; mask, hex(key)\n+    \"\"\"Make sure this is a key without KeyboardModifier mixed in.\"\"\"\n+    key_int = qtutils.extract_enum_val(key)\n+    mask = qtutils.extract_enum_val(Qt.KeyboardModifier.KeyboardModifierMask)\n+    assert not key_int &amp; mask, hex(key_int)\n \n \n def _assert_plain_modifier(key: _ModifierType) -&gt; None:\n     \"\"\"Make sure this is a modifier without a key mixed in.\"\"\"\n-    mask = Qt.KeyboardModifier.KeyboardModifierMask\n-    assert not key &amp; ~mask, hex(key)\n+    key_int = qtutils.extract_enum_val(key)\n+    mask = qtutils.extract_enum_val(Qt.KeyboardModifier.KeyboardModifierMask)\n+    assert not key_int &amp; ~mask, hex(key_int)\n \n \n def _is_printable(key: Qt.Key) -&gt; bool:\n@@ -177,24 +169,6 @@ def _is_printable(key: Qt.Key) -&gt; bool:\n     return key &lt;= 0xff and key not in [Qt.Key.Key_Space, _NIL_KEY]\n \n \n-def is_special(key: Qt.Key, modifiers: _ModifierType) -&gt; bool:\n-    \"\"\"Check whether this key requires special key syntax.\"\"\"\n-    _assert_plain_key(key)\n-    _assert_plain_modifier(modifiers)\n-    return not (_is_printable(key) and\n-                modifiers in [Qt.KeyboardModifier.ShiftModifier, Qt.KeyboardModifier.NoModifier])\n-\n-\n-def is_modifier_key(key: Qt.Key) -&gt; bool:\n-    \"\"\"Test whether the given key is a modifier.\n-\n-    This only considers keys which are part of Qt::KeyboardModifiers, i.e.\n-    which would interrupt a key chain like \"yY\" when handled.\n-    \"\"\"\n-    _assert_plain_key(key)\n-    return key in _MODIFIER_MAP\n-\n-\n def _is_surrogate(key: Qt.Key) -&gt; bool:\n     \"\"\"Check if a codepoint is a UTF-16 surrogate.\n \n@@ -237,8 +211,8 @@ def _check_valid_utf8(s: str, data: Union[Qt.Key, _ModifierType]) -&gt; None:\n     try:\n         s.encode('utf-8')\n     except UnicodeEncodeError as e:  # pragma: no cover\n-        raise ValueError(\"Invalid encoding in 0x{:x} -&gt; {}: {}\"\n-                         .format(int(data), s, e))\n+        i = qtutils.extract_enum_val(data)\n+        raise ValueError(f\"Invalid encoding in 0x{i:x} -&gt; {s}: {e}\")\n \n \n def _key_to_string(key: Qt.Key) -&gt; str:\n@@ -261,7 +235,7 @@ def _key_to_string(key: Qt.Key) -&gt; str:\n \n \n def _modifiers_to_string(modifiers: _ModifierType) -&gt; str:\n-    \"\"\"Convert the given Qt::KeyboardModifiers to a string.\n+    \"\"\"Convert the given Qt::KeyboardModifier to a string.\n \n     Handles Qt.KeyboardModifier.GroupSwitchModifier because Qt doesn't handle that as a\n     modifier.\n@@ -269,12 +243,12 @@ def _modifiers_to_string(modifiers: _ModifierType) -&gt; str:\n     _assert_plain_modifier(modifiers)\n     altgr = Qt.KeyboardModifier.GroupSwitchModifier\n     if modifiers &amp; altgr:\n-        modifiers &amp;= ~altgr\n+        modifiers = _unset_modifier_bits(modifiers, altgr)\n         result = 'AltGr+'\n     else:\n         result = ''\n \n-    result += QKeySequence(_extract_enum_val(modifiers)).toString()\n+    result += QKeySequence(qtutils.extract_enum_val(modifiers)).toString()\n \n     _check_valid_utf8(result, modifiers)\n     return result\n@@ -350,6 +324,21 @@ def _parse_single_key(keystr: str) -&gt; str:\n     return 'Shift+' + keystr if keystr.isupper() else keystr\n \n \n+def _unset_modifier_bits(\n+    modifiers: _ModifierType, mask: _ModifierType\n+) -&gt; _ModifierType:\n+    \"\"\"Unset all bits in modifiers which are given in mask.\n+\n+    Equivalent to modifiers &amp; ~mask, but with a WORKAROUND with PyQt 6,\n+    for a bug in Python 3.11.4 where that isn't possible with an enum.Flag...:\n+    https://github.com/python/cpython/issues/105497\n+    \"\"\"\n+    if machinery.IS_QT5:\n+        return Qt.KeyboardModifiers(modifiers &amp; ~mask)  # can lose type if it's 0\n+    else:\n+        return Qt.KeyboardModifier(modifiers.value &amp; ~mask.value)\n+\n+\n @dataclasses.dataclass(frozen=True, order=True)\n class KeyInfo:\n \n@@ -357,12 +346,35 @@ class KeyInfo:\n \n     Attributes:\n         key: A Qt::Key member.\n-        modifiers: A Qt::KeyboardModifiers enum value.\n+        modifiers: A Qt::KeyboardModifier enum value.\n     \"\"\"\n \n     key: Qt.Key\n     modifiers: _ModifierType = Qt.KeyboardModifier.NoModifier\n \n+    def __post_init__(self) -&gt; None:\n+        \"\"\"Run some validation on the key/modifier values.\"\"\"\n+        # This changed with Qt 6, and e.g. to_qt() relies on this.\n+        if machinery.IS_QT5:\n+            modifier_classes = (Qt.KeyboardModifier, Qt.KeyboardModifiers)\n+        elif machinery.IS_QT6:\n+            modifier_classes = Qt.KeyboardModifier\n+        else:\n+            raise utils.Unreachable()\n+        assert isinstance(self.key, Qt.Key), self.key\n+        assert isinstance(self.modifiers, modifier_classes), self.modifiers\n+\n+        _assert_plain_key(self.key)\n+        _assert_plain_modifier(self.modifiers)\n+\n+    def __repr__(self) -&gt; str:\n+        return utils.get_repr(\n+            self,\n+            key=debug.qenum_key(Qt, self.key, klass=Qt.Key),\n+            modifiers=debug.qflags_key(Qt, self.modifiers, klass=Qt.KeyboardModifier),\n+            text=str(self),\n+        )\n+\n     @classmethod\n     def from_event(cls, e: QKeyEvent) -&gt; 'KeyInfo':\n         \"\"\"Get a KeyInfo object from a QKeyEvent.\n@@ -370,26 +382,33 @@ class KeyInfo:\n         This makes sure that key/modifiers are never mixed and also remaps\n         UTF-16 surrogates to work around QTBUG-72776.\n         \"\"\"\n-        key = _remap_unicode(Qt.Key(e.key()), e.text())\n+        try:\n+            key = Qt.Key(e.key())\n+        except ValueError as ex:\n+            raise InvalidKeyError(str(ex))\n+        key = _remap_unicode(key, e.text())\n         modifiers = e.modifiers()\n-        _assert_plain_key(key)\n-        _assert_plain_modifier(modifiers)\n         return cls(key, modifiers)\n \n     @classmethod\n-    def from_qt(cls, combination: Union[int, QKeyCombination]) -&gt; 'KeyInfo':\n+    def from_qt(cls, combination: _KeyInfoType) -&gt; 'KeyInfo':\n         \"\"\"Construct a KeyInfo from a Qt5-style int or Qt6-style QKeyCombination.\"\"\"\n-        if isinstance(combination, int):\n+        if machinery.IS_QT5:\n+            assert isinstance(combination, int)\n             key = Qt.Key(\n                 int(combination) &amp; ~Qt.KeyboardModifier.KeyboardModifierMask)\n-            modifiers = Qt.KeyboardModifiers(\n+            modifiers = Qt.KeyboardModifier(\n                 int(combination) &amp; Qt.KeyboardModifier.KeyboardModifierMask)\n             return cls(key, modifiers)\n         else:\n             # QKeyCombination is now guaranteed to be available here\n-            assert isinstance(combination, QKeyCombination)  \n+            assert isinstance(combination, QKeyCombination)\n+            try:\n+                key = combination.key()\n+            except ValueError as e:\n+                raise InvalidKeyError(str(e))\n             return cls(\n-                key=combination.key(),\n+                key=key,\n                 modifiers=combination.keyboardModifiers(),\n             )\n \n@@ -404,7 +423,7 @@ class KeyInfo:\n \n         if self.key in _MODIFIER_MAP:\n             # Don't return e.g. \n-            modifiers &amp;= ~_MODIFIER_MAP[self.key]\n+            modifiers = _unset_modifier_bits(modifiers, _MODIFIER_MAP[self.key])\n         elif _is_printable(self.key):\n             # \"normal\" binding\n             if not key_string:  # pragma: no cover\n@@ -413,10 +432,10 @@ class KeyInfo:\n \n             assert len(key_string) == 1, key_string\n             if self.modifiers == Qt.KeyboardModifier.ShiftModifier:\n-                assert not is_special(self.key, self.modifiers)\n+                assert not self.is_special()\n                 return key_string.upper()\n             elif self.modifiers == Qt.KeyboardModifier.NoModifier:\n-                assert not is_special(self.key, self.modifiers)\n+                assert not self.is_special()\n                 return key_string.lower()\n             else:\n                 # Use special binding syntax, but  instead of \n@@ -425,7 +444,7 @@ class KeyInfo:\n         modifiers = Qt.KeyboardModifier(modifiers)\n \n         # \"special\" binding\n-        assert is_special(self.key, self.modifiers)\n+        assert self.is_special()\n         modifier_string = _modifiers_to_string(modifiers)\n         return '&lt;{}{}&gt;'.format(modifier_string, key_string)\n \n@@ -454,15 +473,34 @@ class KeyInfo:\n         \"\"\"Get a QKeyEvent from this KeyInfo.\"\"\"\n         return QKeyEvent(typ, self.key, self.modifiers, self.text())\n \n-    def to_qt(self) -&gt; Union[int, QKeyCombination]:\n+    def to_qt(self) -&gt; _KeyInfoType:\n         \"\"\"Get something suitable for a QKeySequence.\"\"\"\n-        if QKeyCombination is None:\n-            # Qt 5\n+        if machinery.IS_QT5:\n             return int(self.key) | int(self.modifiers)\n-        return QKeyCombination(self.modifiers, self.key)\n+        else:\n+            return QKeyCombination(self.modifiers, self.key)\n \n     def with_stripped_modifiers(self, modifiers: Qt.KeyboardModifier) -&gt; \"KeyInfo\":\n-        return KeyInfo(key=self.key, modifiers=self.modifiers &amp; ~modifiers)\n+        mods = _unset_modifier_bits(self.modifiers, modifiers)\n+        return KeyInfo(key=self.key, modifiers=mods)\n+\n+    def is_special(self) -&gt; bool:\n+        \"\"\"Check whether this key requires special key syntax.\"\"\"\n+        return not (\n+            _is_printable(self.key) and\n+            self.modifiers in [\n+                Qt.KeyboardModifier.ShiftModifier,\n+                Qt.KeyboardModifier.NoModifier,\n+            ]\n+        )\n+\n+    def is_modifier_key(self) -&gt; bool:\n+        \"\"\"Test whether the given key is a modifier.\n+\n+        This only considers keys which are part of Qt::KeyboardModifier, i.e.\n+        which would interrupt a key chain like \"yY\" when handled.\n+        \"\"\"\n+        return self.key in _MODIFIER_MAP\n \n \n class KeySequence:\n@@ -487,7 +525,11 @@ class KeySequence:\n     def __init__(self, *keys: KeyInfo) -&gt; None:\n         self._sequences: List[QKeySequence] = []\n         for sub in utils.chunk(keys, self._MAX_LEN):\n-            args = [info.to_qt() for info in sub]\n+            try:\n+                args = [info.to_qt() for info in sub]\n+            except InvalidKeyError as e:\n+                raise KeyParseError(keystr=None, error=f\"Got invalid key: {e}\")\n+\n             sequence = QKeySequence(*args)\n             self._sequences.append(sequence)\n         if keys:\n@@ -502,7 +544,10 @@ class KeySequence:\n \n     def __iter__(self) -&gt; Iterator[KeyInfo]:\n         \"\"\"Iterate over KeyInfo objects.\"\"\"\n-        for combination in itertools.chain.from_iterable(self._sequences):\n+        # FIXME:mypy Stubs seem to be unaware that iterating a QKeySequence produces\n+        # _KeyInfoType\n+        sequences = cast(List[Iterable[_KeyInfoType]], self._sequences)\n+        for combination in itertools.chain.from_iterable(sequences):\n             yield KeyInfo.from_qt(combination)\n \n     def __repr__(self) -&gt; str:\n@@ -555,9 +600,12 @@ class KeySequence:\n             return infos[item]\n \n     def _validate(self, keystr: str = None) -&gt; None:\n-        for info in self:\n-            if info.key &lt; Qt.Key.Key_Space or info.key &gt;= Qt.Key.Key_unknown:\n-                raise KeyParseError(keystr, \"Got invalid key!\")\n+        try:\n+            for info in self:\n+                if info.key &lt; Qt.Key.Key_Space or info.key &gt;= Qt.Key.Key_unknown:\n+                    raise KeyParseError(keystr, \"Got invalid key!\")\n+        except InvalidKeyError as e:\n+            raise KeyParseError(keystr, f\"Got invalid key: {e}\")\n \n         for seq in self._sequences:\n             if not seq:\n@@ -600,20 +648,23 @@ class KeySequence:\n \n     def append_event(self, ev: QKeyEvent) -&gt; 'KeySequence':\n         \"\"\"Create a new KeySequence object with the given QKeyEvent added.\"\"\"\n-        key = Qt.Key(ev.key())\n+        try:\n+            key = Qt.Key(ev.key())\n+        except ValueError as e:\n+            raise KeyParseError(None, f\"Got invalid key: {e}\")\n \n         _assert_plain_key(key)\n         _assert_plain_modifier(ev.modifiers())\n \n         key = _remap_unicode(key, ev.text())\n-        modifiers = ev.modifiers()\n+        modifiers: _ModifierType = ev.modifiers()\n \n         if key == _NIL_KEY:\n             raise KeyParseError(None, \"Got nil key!\")\n \n         # We always remove Qt.KeyboardModifier.GroupSwitchModifier because QKeySequence has no\n         # way to mention that in a binding anyways...\n-        modifiers &amp;= ~Qt.KeyboardModifier.GroupSwitchModifier\n+        modifiers = _unset_modifier_bits(modifiers, Qt.KeyboardModifier.GroupSwitchModifier)\n \n         # We change Qt.Key.Key_Backtab to Key_Tab here because nobody would\n         # configure \"Shift-Backtab\" in their config.\n@@ -630,7 +681,8 @@ class KeySequence:\n         #\n         # In addition, Shift also *is* relevant when other modifiers are\n         # involved. Shift-Ctrl-X should not be equivalent to Ctrl-X.\n-        if (modifiers == Qt.KeyboardModifier.ShiftModifier and\n+        shift_modifier = Qt.KeyboardModifier.ShiftModifier\n+        if (modifiers == shift_modifier and\n                 _is_printable(key) and\n                 not ev.text().isupper()):\n             modifiers = Qt.KeyboardModifier.NoModifier\n@@ -645,10 +697,10 @@ class KeySequence:\n             if modifiers &amp; Qt.KeyboardModifier.ControlModifier and modifiers &amp; Qt.KeyboardModifier.MetaModifier:\n                 pass\n             elif modifiers &amp; Qt.KeyboardModifier.ControlModifier:\n-                modifiers &amp;= ~Qt.KeyboardModifier.ControlModifier\n+                modifiers = _unset_modifier_bits(modifiers, Qt.KeyboardModifier.ControlModifier)\n                 modifiers |= Qt.KeyboardModifier.MetaModifier\n             elif modifiers &amp; Qt.KeyboardModifier.MetaModifier:\n-                modifiers &amp;= ~Qt.KeyboardModifier.MetaModifier\n+                modifiers = _unset_modifier_bits(modifiers, Qt.KeyboardModifier.MetaModifier)\n                 modifiers |= Qt.KeyboardModifier.ControlModifier\n \n         infos = list(self)\n@@ -667,7 +719,7 @@ class KeySequence:\n             mappings: Mapping['KeySequence', 'KeySequence']\n     ) -&gt; 'KeySequence':\n         \"\"\"Get a new KeySequence with the given mappings applied.\"\"\"\n-        infos = []\n+        infos: List[KeyInfo] = []\n         for info in self:\n             key_seq = KeySequence(info)\n             if key_seq in mappings:\ndiff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py\nindex fdef7c669..69198ecfb 100644\n--- a/qutebrowser/keyinput/macros.py\n+++ b/qutebrowser/keyinput/macros.py\n@@ -1,22 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-# Copyright 2016-2018 Jan Verbeek (blyxxyz) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Jan Verbeek (blyxxyz) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Keyboard macro system.\"\"\"\n \ndiff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py\nindex 694a4ac12..f0337ec88 100644\n--- a/qutebrowser/keyinput/modeman.py\n+++ b/qutebrowser/keyinput/modeman.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Mode manager (per window) which handles the current keyboard mode.\"\"\"\n \n@@ -23,6 +8,7 @@ import functools\n import dataclasses\n from typing import Mapping, Callable, MutableMapping, Union, Set, cast\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import pyqtSlot, pyqtSignal, Qt, QObject, QEvent\n from qutebrowser.qt.gui import QKeyEvent, QKeySequence\n \n@@ -30,7 +16,7 @@ from qutebrowser.commands import runners\n from qutebrowser.keyinput import modeparsers, basekeyparser\n from qutebrowser.config import config\n from qutebrowser.api import cmdutils\n-from qutebrowser.utils import usertypes, log, objreg, utils\n+from qutebrowser.utils import usertypes, log, objreg, utils, qtutils\n from qutebrowser.browser import hints\n from qutebrowser.misc import objects\n \n@@ -51,17 +37,19 @@ class KeyEvent:\n     press/release.\n \n     Attributes:\n-        key: A Qt.Key member (QKeyEvent::key).\n+        key: Usually a Qt.Key member, but could be other ints (QKeyEvent::key).\n         text: A string (QKeyEvent::text).\n     \"\"\"\n \n-    key: Qt.Key\n+    # int instead of Qt.Key:\n+    # WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044607.html\n+    key: int\n     text: str\n \n     @classmethod\n     def from_event(cls, event: QKeyEvent) -&gt; 'KeyEvent':\n         \"\"\"Initialize a KeyEvent from a QKeyEvent.\"\"\"\n-        return cls(Qt.Key(event.key()), event.text())\n+        return cls(event.key(), event.text())\n \n \n class NotInModeError(Exception):\n@@ -289,10 +277,18 @@ class ModeManager(QObject):\n                             \"{}\".format(curmode, utils.qualname(parser)))\n         match = parser.handle(event, dry_run=dry_run)\n \n-        has_modifier = event.modifiers() not in [\n-            Qt.KeyboardModifier.NoModifier,\n-            Qt.KeyboardModifier.ShiftModifier,\n-        ]  # type: ignore[comparison-overlap]\n+        if machinery.IS_QT5:  # FIXME:v4 needed for Qt 5 typing\n+            ignored_modifiers = [\n+                cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.NoModifier),\n+                cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.ShiftModifier),\n+            ]\n+        else:\n+            ignored_modifiers = [\n+                Qt.KeyboardModifier.NoModifier,\n+                Qt.KeyboardModifier.ShiftModifier,\n+            ]\n+        has_modifier = event.modifiers() not in ignored_modifiers\n+\n         is_non_alnum = has_modifier or not event.text().strip()\n \n         forward_unbound_keys = config.cache['input.forward_unbound_keys']\n@@ -312,10 +308,10 @@ class ModeManager(QObject):\n             focus_widget = objects.qapp.focusWidget()\n             log.modes.debug(\"match: {}, forward_unbound_keys: {}, \"\n                             \"passthrough: {}, is_non_alnum: {}, dry_run: {} \"\n-                            \"--&gt; filter: {} (focused: {!r})\".format(\n+                            \"--&gt; filter: {} (focused: {})\".format(\n                                 match, forward_unbound_keys,\n                                 parser.passthrough, is_non_alnum, dry_run,\n-                                filter_this, focus_widget))\n+                                filter_this, qtutils.qobj_repr(focus_widget)))\n         return filter_this\n \n     def _handle_keyrelease(self, event: QKeyEvent) -&gt; bool:\ndiff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py\nindex fc4276b17..05e560111 100644\n--- a/qutebrowser/keyinput/modeparsers.py\n+++ b/qutebrowser/keyinput/modeparsers.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"KeyChainParser for \"hint\" and \"normal\" modes.\n \n@@ -99,6 +84,7 @@ class NormalKeyParser(CommandKeyParser):\n         self._inhibited = False\n         self._inhibited_timer = usertypes.Timer(self, 'normal-inhibited')\n         self._inhibited_timer.setSingleShot(True)\n+        self._inhibited_timer.timeout.connect(self._clear_inhibited)\n \n     def __repr__(self) -&gt; str:\n         return utils.get_repr(self)\n@@ -128,7 +114,6 @@ class NormalKeyParser(CommandKeyParser):\n                 timeout))\n             self._inhibited = True\n             self._inhibited_timer.setInterval(timeout)\n-            self._inhibited_timer.timeout.connect(self._clear_inhibited)\n             self._inhibited_timer.start()\n \n     @pyqtSlot()\n@@ -269,8 +254,10 @@ class RegisterKeyParser(CommandKeyParser):\n                  mode: usertypes.KeyMode,\n                  commandrunner: 'runners.CommandRunner',\n                  parent: QObject = None) -&gt; None:\n-        super().__init__(mode=usertypes.KeyMode.register, win_id=win_id,\n-                         commandrunner=commandrunner, parent=parent,\n+        super().__init__(mode=usertypes.KeyMode.register,\n+                         win_id=win_id,\n+                         commandrunner=commandrunner,\n+                         parent=parent,\n                          supports_count=False)\n         self._register_mode = mode\n \n@@ -281,7 +268,13 @@ class RegisterKeyParser(CommandKeyParser):\n         if match != QKeySequence.SequenceMatch.NoMatch or dry_run:\n             return match\n \n-        if keyutils.is_special(Qt.Key(e.key()), e.modifiers()):\n+        try:\n+            info = keyutils.KeyInfo.from_event(e)\n+        except keyutils.InvalidKeyError as ex:\n+            # See https://github.com/qutebrowser/qutebrowser/issues/7047\n+            log.keyboard.debug(f\"Got invalid key: {ex}\")\n+            return QKeySequence.SequenceMatch.NoMatch\n+        if info.is_special():\n             # this is not a proper register key, let it pass and keep going\n             return QKeySequence.SequenceMatch.NoMatch\n \ndiff --git a/qutebrowser/mainwindow/__init__.py b/qutebrowser/mainwindow/__init__.py\nindex 086ff97cf..c69ac44ba 100644\n--- a/qutebrowser/mainwindow/__init__.py\n+++ b/qutebrowser/mainwindow/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Widgets needed for the main window.\"\"\"\ndiff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py\nindex b9d013cbf..e39ac4f9a 100644\n--- a/qutebrowser/mainwindow/mainwindow.py\n+++ b/qutebrowser/mainwindow/mainwindow.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The main window of qutebrowser.\"\"\"\n \n@@ -25,6 +10,7 @@ import itertools\n import functools\n from typing import List, MutableSequence, Optional, Tuple, cast\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import (pyqtBoundSignal, pyqtSlot, QRect, QPoint, QTimer, Qt,\n                           QCoreApplication, QEventLoop, QByteArray)\n from qutebrowser.qt.widgets import QWidget, QVBoxLayout, QSizePolicy\n@@ -48,7 +34,7 @@ win_id_gen = itertools.count(0)\n \n def get_window(*, via_ipc: bool,\n                target: str,\n-               no_raise: bool = False) -&gt; int:\n+               no_raise: bool = False) -&gt; \"MainWindow\":\n     \"\"\"Helper function for app.py to get a window id.\n \n     Args:\n@@ -58,32 +44,27 @@ def get_window(*, via_ipc: bool,\n         no_raise: suppress target window raising\n \n     Return:\n-        ID of a window that was used to open URL\n+        The MainWindow that was used to open URL\n     \"\"\"\n     if not via_ipc:\n         # Initial main window\n-        return 0\n+        return objreg.get(\"main-window\", scope=\"window\", window=0)\n \n     window = None\n-    should_raise = False\n \n     # Try to find the existing tab target if opening in a tab\n     if target not in {'window', 'private-window'}:\n         window = get_target_window()\n-        should_raise = target not in {'tab-silent', 'tab-bg-silent'}\n+        window.should_raise = target not in {'tab-silent', 'tab-bg-silent'} and not no_raise\n \n     is_private = target == 'private-window'\n \n     # Otherwise, or if no window was found, create a new one\n     if window is None:\n         window = MainWindow(private=is_private)\n-        window.show()\n-        should_raise = True\n+        window.should_raise = not no_raise\n \n-    if should_raise and not no_raise:\n-        raise_window(window)\n-\n-    return window.win_id\n+    return window\n \n \n def raise_window(window, alert=True):\n@@ -95,10 +76,11 @@ def raise_window(window, alert=True):\n     QCoreApplication.processEvents(\n         QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents | QEventLoop.ProcessEventsFlag.ExcludeSocketNotifiers)\n \n-    if not sip.isdeleted(window):\n+    if sip.isdeleted(window):\n         # Could be deleted by the events run above\n-        window.activateWindow()\n+        return\n \n+    window.activateWindow()\n     if alert:\n         objects.qapp.alert(window)\n \n@@ -132,6 +114,8 @@ class MainWindow(QWidget):\n         status: The StatusBar widget.\n         tabbed_browser: The TabbedBrowser widget.\n         state_before_fullscreen: window state before activation of fullscreen.\n+        should_raise: Whether the window should be raised/activated when maybe_raise()\n+                      gets called.\n         _downloadview: The DownloadView widget.\n         _download_model: The DownloadModel instance.\n         _vbox: The main QVBoxLayout.\n@@ -154,6 +138,18 @@ class MainWindow(QWidget):\n             padding-bottom: {{ conf.hints.padding['bottom'] }}px;\n         }\n \n+        QToolTip {\n+            {% if conf.fonts.tooltip %}\n+                font: {{ conf.fonts.tooltip }};\n+            {% endif %}\n+            {% if conf.colors.tooltip.bg %}\n+                background-color: {{ conf.colors.tooltip.bg }};\n+            {% endif %}\n+            {% if conf.colors.tooltip.fg %}\n+                color: {{ conf.colors.tooltip.fg }};\n+            {% endif %}\n+        }\n+\n         QMenu {\n             {% if conf.fonts.contextmenu %}\n                 font: {{ conf.fonts.contextmenu }};\n@@ -279,6 +275,8 @@ class MainWindow(QWidget):\n         self._set_decoration(config.val.window.hide_decoration)\n \n         self.state_before_fullscreen = self.windowState()\n+        self.should_raise: bool = False\n+\n         stylesheet.set_register(self)\n \n     def _init_geometry(self, geometry):\n@@ -412,7 +410,7 @@ class MainWindow(QWidget):\n             self._set_decoration(config.val.window.hide_decoration)\n \n     def _add_widgets(self):\n-        \"\"\"Add or readd all widgets to the VBox.\"\"\"\n+        \"\"\"Add or re-add all widgets to the VBox.\"\"\"\n         self._vbox.removeWidget(self.tabbed_browser.widget)\n         self._vbox.removeWidget(self._downloadview)\n         self._vbox.removeWidget(self.status)\n@@ -485,6 +483,8 @@ class MainWindow(QWidget):\n         mode_manager = modeman.instance(self.win_id)\n \n         # misc\n+        self._prompt_container.release_focus.connect(\n+            self.tabbed_browser.on_release_focus)\n         self.tabbed_browser.close_window.connect(self.close)\n         mode_manager.entered.connect(hints.on_mode_entered)\n \n@@ -561,16 +561,23 @@ class MainWindow(QWidget):\n             self._completion.on_clear_completion_selection)\n         self.status.cmd.hide_completion.connect(\n             self._completion.hide)\n+        self.status.cmd.hide_cmd.connect(self.tabbed_browser.on_release_focus)\n \n     def _set_decoration(self, hidden):\n         \"\"\"Set the visibility of the window decoration via Qt.\"\"\"\n-        window_flags = Qt.WindowType.Window\n+        if machinery.IS_QT5:  # FIXME:v4 needed for Qt 5 typing\n+            window_flags = cast(Qt.WindowFlags, Qt.WindowType.Window)\n+        else:\n+            window_flags = Qt.WindowType.Window\n+\n         refresh_window = self.isVisible()\n         if hidden:\n-            window_flags |= Qt.WindowType.CustomizeWindowHint | Qt.WindowType.NoDropShadowWindowHint\n+            modifiers = Qt.WindowType.CustomizeWindowHint | Qt.WindowType.NoDropShadowWindowHint\n+            window_flags |= modifiers\n         self.setWindowFlags(window_flags)\n \n-        if utils.is_mac and hidden:\n+        if utils.is_mac and hidden and not qtutils.version_check('6.3', compiled=False):\n+            # WORKAROUND for https://codereview.qt-project.org/c/qt/qtbase/+/371279\n             from ctypes import c_void_p\n             # pylint: disable=import-error\n             from objc import objc_object\n@@ -663,6 +670,12 @@ class MainWindow(QWidget):\n \n         return True\n \n+    def maybe_raise(self) -&gt; None:\n+        \"\"\"Raise the window if self.should_raise is set.\"\"\"\n+        if self.should_raise:\n+            raise_window(self)\n+            self.should_raise = False\n+\n     def closeEvent(self, e):\n         \"\"\"Override closeEvent to display a confirmation if needed.\"\"\"\n         if crashsignal.crash_handler.is_crashing:\ndiff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py\nindex d76a8b0e2..95bbed724 100644\n--- a/qutebrowser/mainwindow/messageview.py\n+++ b/qutebrowser/mainwindow/messageview.py\n@@ -1,27 +1,12 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Showing messages above the statusbar.\"\"\"\n \n from typing import MutableSequence, Optional\n \n-from qutebrowser.qt.core import pyqtSlot, pyqtSignal, QTimer, Qt\n+from qutebrowser.qt.core import pyqtSlot, pyqtSignal, Qt\n from qutebrowser.qt.widgets import QWidget, QVBoxLayout, QLabel, QSizePolicy\n \n from qutebrowser.config import config, stylesheet\n@@ -116,7 +101,7 @@ class MessageView(QWidget):\n         self._vbox.setSpacing(0)\n         self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)\n \n-        self._clear_timer = QTimer()\n+        self._clear_timer = usertypes.Timer()\n         self._clear_timer.timeout.connect(self.clear_messages)\n         config.instance.changed.connect(self._set_clear_timer_interval)\n \ndiff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py\nindex 2d2990e88..92d3cc2ea 100644\n--- a/qutebrowser/mainwindow/prompt.py\n+++ b/qutebrowser/mainwindow/prompt.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Showing prompts above the statusbar.\"\"\"\n \n@@ -30,8 +15,8 @@ from qutebrowser.qt.core import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelI\n                           QItemSelectionModel, QObject, QEventLoop)\n from qutebrowser.qt.widgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,\n                              QLabel, QTreeView, QSizePolicy,\n-                             QSpacerItem)\n-from qutebrowser.qt.gui import QFileSystemModel\n+                             QSpacerItem, QFileIconProvider)\n+from qutebrowser.qt.gui import (QFileSystemModel, QIcon)\n \n from qutebrowser.browser import downloads\n from qutebrowser.config import config, configtypes, configexc, stylesheet\n@@ -131,20 +116,12 @@ class PromptQueue(QObject):\n         \"\"\"Cancel all blocking questions.\n \n         Quits and removes all running event loops.\n-\n-        Return:\n-            True if loops needed to be aborted,\n-            False otherwise.\n         \"\"\"\n-        log.prompt.debug(\"Shutting down with loops {}\".format(self._loops))\n+        log.prompt.debug(f\"Shutting down with loops {self._loops}\")\n         self._shutting_down = True\n-        if self._loops:\n-            for loop in self._loops:\n-                loop.quit()\n-                loop.deleteLater()\n-            return True\n-        else:\n-            return False\n+        for loop in self._loops:\n+            loop.quit()\n+            loop.deleteLater()\n \n     @pyqtSlot(usertypes.Question, bool)\n     def ask_question(self, question, blocking):\n@@ -225,6 +202,7 @@ class PromptQueue(QObject):\n         log.prompt.debug(\"Left mode {}, hiding {}\".format(\n             mode, self._question))\n         self.show_prompts.emit(None)\n+\n         if self._question.answer is None and not self._question.is_aborted:\n             log.prompt.debug(\"Cancelling {} because {} was left\".format(\n                 self._question, mode))\n@@ -284,6 +262,7 @@ class PromptContainer(QWidget):\n         }\n     \"\"\"\n     update_geometry = pyqtSignal()\n+    release_focus = pyqtSignal()\n \n     def __init__(self, win_id, parent=None):\n         super().__init__(parent)\n@@ -310,18 +289,26 @@ class PromptContainer(QWidget):\n         Args:\n             question: A Question object or None.\n         \"\"\"\n-        item = self._layout.takeAt(0)\n-        if item is not None:\n+        item = qtutils.add_optional(self._layout.takeAt(0))\n+        if item is None:\n+            widget = None\n+        else:\n             widget = item.widget()\n-            log.prompt.debug(\"Deleting old prompt {}\".format(widget))\n-            widget.hide()\n+            assert widget is not None\n+            log.prompt.debug(f\"Deleting old prompt {widget!r}\")\n             widget.deleteLater()\n \n         if question is None:\n             log.prompt.debug(\"No prompts left, hiding prompt container.\")\n             self._prompt = None\n+            self.release_focus.emit()\n             self.hide()\n             return\n+        elif widget is not None:\n+            # We have more prompts to show, just hide the old one.\n+            # This needs to happen *after* we possibly hid the entire prompt container,\n+            # so that keyboard focus can be reassigned properly via release_focus.\n+            widget.hide()\n \n         classes = {\n             usertypes.PromptMode.yesno: YesNoPrompt,\n@@ -376,6 +363,7 @@ class PromptContainer(QWidget):\n         item = self._layout.takeAt(0)\n         if item is not None:\n             widget = item.widget()\n+            assert widget is not None\n             log.prompt.debug(\"Deleting prompt {}\".format(widget))\n             widget.hide()\n             widget.deleteLater()\n@@ -645,6 +633,21 @@ class LineEditPrompt(_BasePrompt):\n         return [('prompt-accept', 'Accept'), ('mode-leave', 'Abort')]\n \n \n+class NullIconProvider(QFileIconProvider):\n+\n+    \"\"\"Returns empty icon for everything.\"\"\"\n+\n+    def __init__(self):\n+        super().__init__()\n+        self.null_icon = QIcon()\n+\n+    def icon(self, _t):\n+        return self.null_icon\n+\n+    def type(self, _info):\n+        return 'unknown'\n+\n+\n class FilenamePrompt(_BasePrompt):\n \n     \"\"\"A prompt for a filename.\"\"\"\n@@ -746,6 +749,10 @@ class FilenamePrompt(_BasePrompt):\n     def _init_fileview(self):\n         self._file_view = QTreeView(self)\n         self._file_model = QFileSystemModel(self)\n+\n+        # avoid icon and mime type lookups, they are slow in Qt6\n+        self._file_model.setIconProvider(NullIconProvider())\n+\n         self._file_view.setModel(self._file_model)\n         self._file_view.clicked.connect(self._insert_path)\n \n@@ -760,9 +767,21 @@ class FilenamePrompt(_BasePrompt):\n             self._file_view.setColumnHidden(col, True)\n         # Nothing selected initially\n         self._file_view.setCurrentIndex(QModelIndex())\n-        # The model needs to be sorted so we get the correct first/last index\n-        self._file_model.directoryLoaded.connect(\n-            lambda: self._file_model.sort(0))\n+\n+        self._file_model.directoryLoaded.connect(self.on_directory_loaded)\n+\n+    @pyqtSlot()\n+    def on_directory_loaded(self):\n+        \"\"\"Sort the model after a directory gets loaded.\n+\n+        The model needs to be sorted so we get the correct first/last index.\n+\n+        NOTE: This needs to be a proper @pystSlot() function, and not a lambda.\n+        Otherwise, PyQt seems to fail to disconnect it immediately after the\n+        object gets destroyed, and we get segfaults when deleting the directory\n+        in unit tests.\n+        \"\"\"\n+        self._file_model.sort(0)\n \n     def accept(self, value=None, save=False):\n         self._check_save_support(save)\n@@ -778,6 +797,7 @@ class FilenamePrompt(_BasePrompt):\n         # This duplicates some completion code, but I don't see a nicer way...\n         assert which in ['prev', 'next'], which\n         selmodel = self._file_view.selectionModel()\n+        assert selmodel is not None\n \n         parent = self._file_view.rootIndex()\n         first_index = self._file_model.index(0, 0, parent)\ndiff --git a/qutebrowser/mainwindow/statusbar/__init__.py b/qutebrowser/mainwindow/statusbar/__init__.py\nindex 5ded944da..ecc318265 100644\n--- a/qutebrowser/mainwindow/statusbar/__init__.py\n+++ b/qutebrowser/mainwindow/statusbar/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Widgets needed for the statusbar.\"\"\"\ndiff --git a/qutebrowser/mainwindow/statusbar/backforward.py b/qutebrowser/mainwindow/statusbar/backforward.py\nindex 76614a14f..5e4dd98ed 100644\n--- a/qutebrowser/mainwindow/statusbar/backforward.py\n+++ b/qutebrowser/mainwindow/statusbar/backforward.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Navigation (back/forward) indicator displayed in the statusbar.\"\"\"\n \ndiff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py\nindex 1205b5466..b628a03cc 100644\n--- a/qutebrowser/mainwindow/statusbar/bar.py\n+++ b/qutebrowser/mainwindow/statusbar/bar.py\n@@ -1,28 +1,13 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The main statusbar widget.\"\"\"\n \n import enum\n import dataclasses\n \n-from qutebrowser.qt.core import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer\n+from qutebrowser.qt.core import pyqtSignal, pyqtProperty, pyqtSlot, Qt, QSize, QTimer\n from qutebrowser.qt.widgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy\n \n from qutebrowser.browser import browsertab\n@@ -287,6 +272,8 @@ class StatusBar(QWidget):\n                        self.backforward, self.tabindex,\n                        self.keystring, self.prog, self.clock, *self._text_widgets]:\n             assert isinstance(widget, QWidget)\n+            if widget in [self.prog, self.backforward]:\n+                widget.enabled = False  # type: ignore[attr-defined]\n             widget.hide()\n             self._hbox.removeWidget(widget)\n         self._text_widgets.clear()\n@@ -390,9 +377,11 @@ class StatusBar(QWidget):\n     @pyqtSlot(usertypes.KeyMode)\n     def on_mode_entered(self, mode):\n         \"\"\"Mark certain modes in the commandline.\"\"\"\n-        mode_manager = modeman.instance(self._win_id)\n-        if config.val.statusbar.show == 'in-mode':\n+        if config.val.statusbar.show == 'in-mode' and mode != usertypes.KeyMode.command:\n+            # Showing in command mode is handled via _show_cmd_widget()\n             self.show()\n+\n+        mode_manager = modeman.instance(self._win_id)\n         if mode_manager.parsers[mode].passthrough:\n             self._set_mode_text(mode.name)\n         if mode in [usertypes.KeyMode.insert,\n@@ -406,9 +395,11 @@ class StatusBar(QWidget):\n     @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)\n     def on_mode_left(self, old_mode, new_mode):\n         \"\"\"Clear marked mode.\"\"\"\n-        mode_manager = modeman.instance(self._win_id)\n-        if config.val.statusbar.show == 'in-mode':\n+        if config.val.statusbar.show == 'in-mode' and old_mode != usertypes.KeyMode.command:\n+            # Hiding in command mode is handled via _hide_cmd_widget()\n             self.hide()\n+\n+        mode_manager = modeman.instance(self._win_id)\n         if mode_manager.parsers[old_mode].passthrough:\n             if mode_manager.parsers[new_mode].passthrough:\n                 self._set_mode_text(new_mode.name)\ndiff --git a/qutebrowser/mainwindow/statusbar/clock.py b/qutebrowser/mainwindow/statusbar/clock.py\nindex ebab152b7..604243935 100644\n--- a/qutebrowser/mainwindow/statusbar/clock.py\n+++ b/qutebrowser/mainwindow/statusbar/clock.py\n@@ -1,28 +1,14 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Clock displayed in the statusbar.\"\"\"\n from datetime import datetime\n \n-from qutebrowser.qt.core import Qt, QTimer\n+from qutebrowser.qt.core import Qt\n \n from qutebrowser.mainwindow.statusbar import textbase\n+from qutebrowser.utils import usertypes\n \n \n class Clock(textbase.TextBase):\n@@ -35,7 +21,7 @@ class Clock(textbase.TextBase):\n         super().__init__(parent, elidemode=Qt.TextElideMode.ElideNone)\n         self.format = \"\"\n \n-        self.timer = QTimer(self)\n+        self.timer = usertypes.Timer(self)\n         self.timer.timeout.connect(self._show_time)\n \n     def _show_time(self):\ndiff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py\nindex df85d0dfa..988eed4a0 100644\n--- a/qutebrowser/mainwindow/statusbar/command.py\n+++ b/qutebrowser/mainwindow/statusbar/command.py\n@@ -1,26 +1,12 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The commandline in the statusbar.\"\"\"\n \n-from typing import Optional\n+from typing import Optional, cast\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import pyqtSignal, pyqtSlot, Qt, QSize\n from qutebrowser.qt.gui import QKeyEvent\n from qutebrowser.qt.widgets import QSizePolicy, QWidget\n@@ -33,7 +19,7 @@ from qutebrowser.utils import usertypes, log, objreg, message, utils\n from qutebrowser.config import config\n \n \n-class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n+class Command(misc.CommandLineEdit):\n \n     \"\"\"The commandline part of the statusbar.\n \n@@ -63,8 +49,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n     def __init__(self, *, win_id: int,\n                  private: bool,\n                  parent: QWidget = None) -&gt; None:\n-        misc.CommandLineEdit.__init__(self, parent=parent)\n-        misc.MinimalLineEditMixin.__init__(self)\n+        super().__init__(parent)\n         self._win_id = win_id\n         if not private:\n             command_history = objreg.get('command-history')\n@@ -77,6 +62,17 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n         self.textChanged.connect(self.updateGeometry)\n         self.textChanged.connect(self._incremental_search)\n \n+        self.setStyleSheet(\n+            \"\"\"\n+            QLineEdit {\n+                border: 0px;\n+                padding-left: 1px;\n+                background-color: transparent;\n+            }\n+            \"\"\"\n+        )\n+        self.setAttribute(Qt.WidgetAttribute.WA_MacShowFocusRect, False)\n+\n     def _handle_search(self) -&gt; bool:\n         \"\"\"Check if the currently entered text is a search, and if so, run it.\n \n@@ -102,7 +98,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n         else:\n             return ''\n \n-    def set_cmd_text(self, text: str) -&gt; None:\n+    def cmd_set_text(self, text: str) -&gt; None:\n         \"\"\"Preset the statusbar to some text.\n \n         Args:\n@@ -114,10 +110,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n         self.setFocus()\n         self.show_cmd.emit()\n \n-    @cmdutils.register(instance='status-command', name='set-cmd-text',\n-                       scope='window', maxsplit=0)\n+    @cmdutils.register(instance='status-command', name='cmd-set-text',\n+                       scope='window', maxsplit=0, deprecated_name='set-cmd-text')\n     @cmdutils.argument('count', value=cmdutils.Value.count)\n-    def set_cmd_text_command(self, text: str,\n+    def cmd_set_text_command(self, text: str,\n                              count: int = None,\n                              space: bool = False,\n                              append: bool = False,\n@@ -126,7 +122,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n \n         //\n \n-        Wrapper for set_cmd_text to check the arguments and allow multiple\n+        Wrapper for cmd_set_text to check the arguments and allow multiple\n         strings which will get joined.\n \n         Args:\n@@ -150,7 +146,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n         if run_on_count and count is not None:\n             self.got_cmd[str, int].emit(text, count)\n         else:\n-            self.set_cmd_text(text)\n+            self.cmd_set_text(text)\n \n     @cmdutils.register(instance='status-command',\n                        modes=[usertypes.KeyMode.command], scope='window')\n@@ -165,7 +161,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n                 cmdhistory.HistoryEndReachedError):\n             return\n         if item:\n-            self.set_cmd_text(item)\n+            self.cmd_set_text(item)\n \n     @cmdutils.register(instance='status-command',\n                        modes=[usertypes.KeyMode.command], scope='window')\n@@ -178,7 +174,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n         except cmdhistory.HistoryEndReachedError:\n             return\n         if item:\n-            self.set_cmd_text(item)\n+            self.cmd_set_text(item)\n \n     @cmdutils.register(instance='status-command',\n                        modes=[usertypes.KeyMode.command], scope='window')\n@@ -201,8 +197,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n         if not was_search:\n             self.got_cmd[str].emit(text[1:])\n \n-    @cmdutils.register(instance='status-command', scope='window')\n-    def edit_command(self, run: bool = False) -&gt; None:\n+    @cmdutils.register(instance='status-command', scope='window',\n+                       deprecated_name='edit-command')\n+    def cmd_edit(self, run: bool = False) -&gt; None:\n         \"\"\"Open an editor to modify the current command.\n \n         Args:\n@@ -216,7 +213,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n                 message.error('command must start with one of {}'\n                               .format(modeparsers.STARTCHARS))\n                 return\n-            self.set_cmd_text(text)\n+            self.cmd_set_text(text)\n             if run:\n                 self.command_accept()\n \n@@ -250,24 +247,39 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):\n         else:\n             raise utils.Unreachable(\"setText got called with invalid text \"\n                                     \"'{}'!\".format(text))\n+        # FIXME:mypy PyQt6 stubs issue\n+        if machinery.IS_QT6:\n+            text = cast(str, text)\n         super().setText(text)\n \n-    def keyPressEvent(self, e: QKeyEvent) -&gt; None:\n-        \"\"\"Override keyPressEvent to ignore Return key presses.\n+    def keyPressEvent(self, e: Optional[QKeyEvent]) -&gt; None:\n+        \"\"\"Override keyPressEvent to ignore Return key presses, and add Shift-Ins.\n \n         If this widget is focused, we are in passthrough key mode, and\n         Enter/Shift+Enter/etc. will cause QLineEdit to think it's finished\n         without command_accept to be called.\n         \"\"\"\n+        assert e is not None\n+        if machinery.IS_QT5:  # FIXME:v4 needed for Qt 5 typing\n+            shift = cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.ShiftModifier)\n+        else:\n+            shift = Qt.KeyboardModifier.ShiftModifier\n+\n         text = self.text()\n         if text in modeparsers.STARTCHARS and e.key() == Qt.Key.Key_Backspace:\n             e.accept()\n             modeman.leave(self._win_id, usertypes.KeyMode.command,\n                           'prefix deleted')\n-            return\n-        if e.key() == Qt.Key.Key_Return:\n+        elif e.key() == Qt.Key.Key_Return:\n             e.ignore()\n-            return\n+        elif e.key() == Qt.Key.Key_Insert and e.modifiers() == shift:\n+            try:\n+                text = utils.get_clipboard(selection=True, fallback=True)\n+            except utils.ClipboardError:\n+                e.ignore()\n+            else:\n+                e.accept()\n+                self.insert(text)\n         else:\n             super().keyPressEvent(e)\n \ndiff --git a/qutebrowser/mainwindow/statusbar/keystring.py b/qutebrowser/mainwindow/statusbar/keystring.py\nindex ed8b56318..9be36dc3a 100644\n--- a/qutebrowser/mainwindow/statusbar/keystring.py\n+++ b/qutebrowser/mainwindow/statusbar/keystring.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Keychain string displayed in the statusbar.\"\"\"\n \ndiff --git a/qutebrowser/mainwindow/statusbar/percentage.py b/qutebrowser/mainwindow/statusbar/percentage.py\nindex 6ecdc4659..d727a03dd 100644\n--- a/qutebrowser/mainwindow/statusbar/percentage.py\n+++ b/qutebrowser/mainwindow/statusbar/percentage.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Scroll percentage displayed in the statusbar.\"\"\"\n \ndiff --git a/qutebrowser/mainwindow/statusbar/progress.py b/qutebrowser/mainwindow/statusbar/progress.py\nindex f76367b38..3bc8cdf4a 100644\n--- a/qutebrowser/mainwindow/statusbar/progress.py\n+++ b/qutebrowser/mainwindow/statusbar/progress.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The progress bar in the statusbar.\"\"\"\n \ndiff --git a/qutebrowser/mainwindow/statusbar/searchmatch.py b/qutebrowser/mainwindow/statusbar/searchmatch.py\nindex 7c9a5b05e..aaee5aed9 100644\n--- a/qutebrowser/mainwindow/statusbar/searchmatch.py\n+++ b/qutebrowser/mainwindow/statusbar/searchmatch.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The search match indicator in the statusbar.\"\"\"\n \ndiff --git a/qutebrowser/mainwindow/statusbar/tabindex.py b/qutebrowser/mainwindow/statusbar/tabindex.py\nindex e41501123..59dadfb1e 100644\n--- a/qutebrowser/mainwindow/statusbar/tabindex.py\n+++ b/qutebrowser/mainwindow/statusbar/tabindex.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"TabIndex displayed in the statusbar.\"\"\"\n \ndiff --git a/qutebrowser/mainwindow/statusbar/textbase.py b/qutebrowser/mainwindow/statusbar/textbase.py\nindex 9ab4b9753..734073df2 100644\n--- a/qutebrowser/mainwindow/statusbar/textbase.py\n+++ b/qutebrowser/mainwindow/statusbar/textbase.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Base text widgets for statusbar.\"\"\"\n \ndiff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py\nindex 27a2b2133..0debabcd6 100644\n--- a/qutebrowser/mainwindow/statusbar/url.py\n+++ b/qutebrowser/mainwindow/statusbar/url.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"URL displayed in the statusbar.\"\"\"\n \n@@ -118,7 +103,9 @@ class UrlText(textbase.TextBase):\n         if old_urltype != self._urltype:\n             # We can avoid doing an unpolish here because the new style will\n             # always override the old one.\n-            self.style().polish(self)\n+            style = self.style()\n+            assert style is not None\n+            style.polish(self)\n \n     @pyqtSlot(usertypes.LoadStatus)\n     def on_load_status_changed(self, status):\ndiff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py\nindex bce66db2a..47d8dc680 100644\n--- a/qutebrowser/mainwindow/tabbedbrowser.py\n+++ b/qutebrowser/mainwindow/tabbedbrowser.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The main tabbed browser widget.\"\"\"\n \n@@ -34,9 +19,9 @@ from qutebrowser.config import config\n from qutebrowser.keyinput import modeman\n from qutebrowser.mainwindow import tabwidget, mainwindow\n from qutebrowser.browser import signalfilter, browsertab, history\n-from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,\n+from qutebrowser.utils import (log, usertypes, utils, qtutils,\n                                urlutils, message, jinja, version)\n-from qutebrowser.misc import quitter\n+from qutebrowser.misc import quitter, objects\n \n \n @dataclasses.dataclass\n@@ -221,15 +206,21 @@ class TabbedBrowser(QWidget):\n         self._tab_insert_idx_right = -1\n         self.is_shutting_down = False\n         self.widget.tabCloseRequested.connect(self.on_tab_close_requested)\n-        self.widget.new_tab_requested.connect(self.tabopen)\n+        self.widget.new_tab_requested.connect(\n+            self.tabopen)  # type: ignore[arg-type,unused-ignore]\n         self.widget.currentChanged.connect(self._on_current_changed)\n-        self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide)\n+        self.cur_fullscreen_requested.connect(self.widget.tab_bar().maybe_hide)\n \n         self.widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)\n \n-        # load_finished instead of load_started as WORKAROUND for\n-        # https://bugreports.qt.io/browse/QTBUG-65223\n-        self.cur_load_finished.connect(self._leave_modes_on_load)\n+        if (\n+            objects.backend == usertypes.Backend.QtWebEngine and\n+            version.qtwebengine_versions().webengine &lt; utils.VersionNumber(5, 15, 5)\n+        ):\n+            # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223\n+            self.cur_load_finished.connect(self._leave_modes_on_load)\n+        else:\n+            self.cur_load_started.connect(self._leave_modes_on_load)\n \n         # handle mode_override\n         self.current_tab_changed.connect(lambda tab: self._mode_override(tab.url()))\n@@ -245,7 +236,7 @@ class TabbedBrowser(QWidget):\n         self.search_options: Mapping[str, Any] = {}\n         self._local_marks: MutableMapping[QUrl, MutableMapping[str, QPoint]] = {}\n         self._global_marks: MutableMapping[str, Tuple[QPoint, QUrl]] = {}\n-        self.default_window_icon = self.widget.window().windowIcon()\n+        self.default_window_icon = self._window().windowIcon()\n         self.is_private = private\n         self.tab_deque = TabDeque()\n         config.instance.changed.connect(self._on_config_changed)\n@@ -297,10 +288,9 @@ class TabbedBrowser(QWidget):\n         \"\"\"\n         widgets = []\n         for i in range(self.widget.count()):\n-            widget = self.widget.widget(i)\n+            widget = qtutils.add_optional(self.widget.widget(i))\n             if widget is None:\n-                log.webview.debug(  # type: ignore[unreachable]\n-                    \"Got None-widget in tabbedbrowser!\")\n+                log.webview.debug(\"Got None-widget in tabbedbrowser!\")\n             else:\n                 widgets.append(widget)\n         return widgets\n@@ -326,7 +316,10 @@ class TabbedBrowser(QWidget):\n         fields['id'] = self._win_id\n \n         title = title_format.format(**fields)\n-        self.widget.window().setWindowTitle(title)\n+        # prevent hanging WMs and similar issues with giant URLs\n+        title = utils.elide(title, 1024)\n+\n+        self._window().setWindowTitle(title)\n \n     def _connect_tab_signals(self, tab):\n         \"\"\"Set up the needed signals for tab.\"\"\"\n@@ -392,6 +385,15 @@ class TabbedBrowser(QWidget):\n         assert isinstance(tab, browsertab.AbstractTab), tab\n         return tab\n \n+    def _window(self) -&gt; QWidget:\n+        \"\"\"Get the current window widget.\n+\n+        Note: This asserts if there is no window.\n+        \"\"\"\n+        window = self.widget.window()\n+        assert window is not None\n+        return window\n+\n     def _tab_by_idx(self, idx: int) -&gt; Optional[browsertab.AbstractTab]:\n         \"\"\"Get a browser tab by index.\n \n@@ -559,6 +561,7 @@ class TabbedBrowser(QWidget):\n \n             newtab.history.private_api.deserialize(entry.history)\n             newtab.set_pinned(entry.pinned)\n+            newtab.setFocus()\n \n     @pyqtSlot('QUrl', bool)\n     def load_url(self, url, newtab):\n@@ -634,11 +637,10 @@ class TabbedBrowser(QWidget):\n \n         if config.val.tabs.tabs_are_windows and self.widget.count() &gt; 0:\n             window = mainwindow.MainWindow(private=self.is_private)\n+            tab = window.tabbed_browser.tabopen(\n+                url=url, background=background, related=related)\n             window.show()\n-            tabbed_browser = objreg.get('tabbed-browser', scope='window',\n-                                        window=window.win_id)\n-            return tabbed_browser.tabopen(url=url, background=background,\n-                                          related=related)\n+            return tab\n \n         tab = browsertab.create(win_id=self._win_id,\n                                 private=self.is_private,\n@@ -658,11 +660,12 @@ class TabbedBrowser(QWidget):\n             # Make sure the background tab has the correct initial size.\n             # With a foreground tab, it's going to be resized correctly by the\n             # layout anyways.\n-            tab.resize(self.widget.currentWidget().size())\n+            current_widget = self._current_tab()\n+            tab.resize(current_widget.size())\n             self.widget.tab_index_changed.emit(self.widget.currentIndex(),\n                                                self.widget.count())\n             # Refocus webview in case we lost it by spawning a bg tab\n-            self.widget.currentWidget().setFocus()\n+            current_widget.setFocus()\n         else:\n             self.widget.setCurrentWidget(tab)\n \n@@ -735,7 +738,7 @@ class TabbedBrowser(QWidget):\n             tab.data.keep_icon = False\n         elif (config.cache['tabs.tabs_are_windows'] and\n               tab.data.should_show_icon()):\n-            self.widget.window().setWindowIcon(self.default_window_icon)\n+            self._window().setWindowIcon(self.default_window_icon)\n \n     @pyqtSlot()\n     def _on_load_status_changed(self, tab):\n@@ -855,19 +858,34 @@ class TabbedBrowser(QWidget):\n                 assert isinstance(tab, browsertab.AbstractTab), tab\n                 tab.data.input_mode = mode\n \n-    @pyqtSlot(usertypes.KeyMode)\n-    def on_mode_left(self, mode):\n-        \"\"\"Give focus to current tab if command mode was left.\"\"\"\n-        widget = self.widget.currentWidget()\n+    @pyqtSlot()\n+    def on_release_focus(self):\n+        \"\"\"Give keyboard focus to the current tab when requested by statusbar/prompt.\n+\n+        This gets emitted by the statusbar and prompt container before they call .hide()\n+        on themselves, with the idea that we can explicitly reassign the focus,\n+        instead of Qt implicitly calling its QWidget::focusNextPrevChild() method,\n+        finding a new widget to give keyboard focus to.\n+        \"\"\"\n+        widget = qtutils.add_optional(self.widget.currentWidget())\n+        if widget is None:\n+            return\n+\n+        log.modes.debug(f\"Focus released, focusing {widget!r}\")\n+        widget.setFocus()\n+\n+    @pyqtSlot()\n+    def on_mode_left(self):\n+        \"\"\"Save input mode for restoring if needed.\"\"\"\n+        if config.val.tabs.mode_on_change != 'restore':\n+            return\n+\n+        widget = qtutils.add_optional(self.widget.currentWidget())\n         if widget is None:\n-            return  # type: ignore[unreachable]\n-        if mode in [usertypes.KeyMode.command] + modeman.PROMPT_MODES:\n-            log.modes.debug(\"Left status-input mode, focusing {!r}\".format(\n-                widget))\n-            widget.setFocus()\n-        if config.val.tabs.mode_on_change == 'restore':\n-            assert isinstance(widget, browsertab.AbstractTab), widget\n-            widget.data.input_mode = usertypes.KeyMode.normal\n+            return\n+\n+        assert isinstance(widget, browsertab.AbstractTab), widget\n+        widget.data.input_mode = usertypes.KeyMode.normal\n \n     @pyqtSlot(int)\n     def _on_current_changed(self, idx):\n@@ -993,16 +1011,11 @@ class TabbedBrowser(QWidget):\n \n         # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91715\n         versions = version.qtwebengine_versions()\n-        is_qtbug_91715 = (\n+        if (\n             status == browsertab.TerminationStatus.unknown and\n             code == 1002 and\n-            versions.webengine == utils.VersionNumber(5, 15, 3))\n-\n-        def show_error_page(html):\n-            tab.set_html(html)\n-            log.webview.error(msg)\n-\n-        if is_qtbug_91715:\n+            versions.webengine == utils.VersionNumber(5, 15, 3)\n+        ):\n             log.webview.error(msg)\n             log.webview.error('')\n             log.webview.error(\n@@ -1016,12 +1029,17 @@ class TabbedBrowser(QWidget):\n                 'A proper fix is likely available in QtWebEngine soon (which is why '\n                 'the workaround is disabled by default).')\n             log.webview.error('')\n-        else:\n-            url_string = tab.url(requested=True).toDisplayString()\n-            error_page = jinja.render(\n-                'error.html', title=\"Error loading {}\".format(url_string),\n-                url=url_string, error=msg)\n-            QTimer.singleShot(100, lambda: show_error_page(error_page))\n+            return\n+\n+        def show_error_page(html):\n+            tab.set_html(html)\n+            log.webview.error(msg)\n+\n+        url_string = tab.url(requested=True).toDisplayString()\n+        error_page = jinja.render(\n+            'error.html', title=\"Error loading {}\".format(url_string),\n+            url=url_string, error=msg)\n+        QTimer.singleShot(100, lambda: show_error_page(error_page))\n \n     def resizeEvent(self, e):\n         \"\"\"Extend resizeEvent of QWidget to emit a resized signal afterwards.\ndiff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py\nindex 4af9d2f80..afbfa0a95 100644\n--- a/qutebrowser/mainwindow/tabwidget.py\n+++ b/qutebrowser/mainwindow/tabwidget.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The tab widget used for TabbedBrowser from browser.py.\"\"\"\n \n@@ -26,9 +11,9 @@ from typing import Optional, Dict, Any\n \n from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint,\n                           QTimer, QUrl)\n-from qutebrowser.qt.widgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,\n+from qutebrowser.qt.widgets import (QTabWidget, QTabBar, QSizePolicy, QProxyStyle,\n                              QStyle, QStylePainter, QStyleOptionTab,\n-                             QStyleFactory)\n+                             QCommonStyle)\n from qutebrowser.qt.gui import QIcon, QPalette, QColor\n \n from qutebrowser.utils import qtutils, objreg, utils, usertypes, log\n@@ -357,7 +342,9 @@ class TabWidget(QTabWidget):\n         self.setTabIcon(idx, icon)\n \n         if config.val.tabs.tabs_are_windows:\n-            self.window().setWindowIcon(tab.icon())\n+            window = self.window()\n+            assert window is not None\n+            window.setWindowIcon(tab.icon())\n \n     def setTabIcon(self, idx: int, icon: QIcon) -&gt; None:\n         \"\"\"Always show tab icons for pinned tabs in some circumstances.\"\"\"\n@@ -367,7 +354,9 @@ class TabWidget(QTabWidget):\n                 config.cache['tabs.pinned.shrink'] and\n                 not self.tab_bar().vertical and\n                 tab is not None and tab.data.pinned):\n-            icon = self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)\n+            style = self.style()\n+            assert style is not None\n+            icon = style.standardIcon(QStyle.StandardPixmap.SP_FileIcon)\n         super().setTabIcon(idx, icon)\n \n \n@@ -405,9 +394,11 @@ class TabBar(QTabBar):\n     def __init__(self, win_id, parent=None):\n         super().__init__(parent)\n         self._win_id = win_id\n-        self.setStyle(TabBarStyle())\n+        self._our_style = TabBarStyle()\n+        self.setStyle(self._our_style)\n+        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)\n         self.vertical = False\n-        self._auto_hide_timer = QTimer()\n+        self._auto_hide_timer = usertypes.Timer()\n         self._auto_hide_timer.setSingleShot(True)\n         self._auto_hide_timer.timeout.connect(self.maybe_hide)\n         self._on_show_switching_delay_changed()\n@@ -696,6 +687,29 @@ class TabBar(QTabBar):\n         qtutils.ensure_valid(size)\n         return size\n \n+    def initStyleOption(self, opt, idx):\n+        \"\"\"Override QTabBar.initStyleOption().\n+\n+        Used to calculate styling clues from a widget for the GUI layer.\n+        \"\"\"\n+        super().initStyleOption(opt, idx)\n+\n+        # Re-do the text elision that the base QTabBar does, but using a text\n+        # rectangle computed by out TabBarStyle. With Qt6 the base class ends\n+        # up using QCommonStyle directly for that which has a different opinon\n+        # of how vertical tabs should work.\n+        text_rect = self._our_style.subElementRect(\n+            QStyle.SubElement.SE_TabBarTabText,\n+            opt,\n+            self,\n+        )\n+        opt.text = self.fontMetrics().elidedText(\n+            self.tabText(idx),\n+            self.elideMode(),\n+            text_rect.width(),\n+            Qt.TextFlag.TextShowMnemonic,\n+        )\n+\n     def paintEvent(self, event):\n         \"\"\"Override paintEvent to draw the tabs like we want to.\"\"\"\n         p = QStylePainter(self)\n@@ -775,7 +789,7 @@ class Layouts:\n     indicator: QRect\n \n \n-class TabBarStyle(QCommonStyle):\n+class TabBarStyle(QProxyStyle):\n \n     \"\"\"Qt style used by TabBar to fix some issues with the default one.\n \n@@ -783,32 +797,20 @@ class TabBarStyle(QCommonStyle):\n         - Remove the focus rectangle Ubuntu draws on tabs.\n         - Force text to be left-aligned even though Qt has \"centered\"\n           hardcoded.\n-\n-    Unfortunately PyQt doesn't support QProxyStyle, so we need to do this the\n-    hard way...\n-\n-    Based on:\n-\n-    https://stackoverflow.com/a/17294081\n-    https://code.google.com/p/makehuman/source/browse/trunk/makehuman/lib/qtgui.py\n     \"\"\"\n \n     ICON_PADDING = 4\n \n-    def __init__(self):\n-        \"\"\"Initialize all functions we're not overriding.\n+    def __init__(self, style=None):\n+        # \"useless\" override as WORKAROUND for\n+        # https://www.riverbankcomputing.com/pipermail/pyqt/2023-September/045510.html\n+        super().__init__(style)\n \n-        This simply calls the corresponding function in self._style.\n-        \"\"\"\n-        self._style = QStyleFactory.create('Fusion')\n-        for method in ['drawComplexControl', 'drawItemPixmap',\n-                       'generatedIconPixmap', 'hitTestComplexControl',\n-                       'itemPixmapRect', 'itemTextRect', 'polish', 'styleHint',\n-                       'subControlRect', 'unpolish', 'drawItemText',\n-                       'sizeFromContents', 'drawPrimitive']:\n-            target = getattr(self._style, method)\n-            setattr(self, method, functools.partial(target))\n-        super().__init__()\n+    def _base_style(self) -&gt; QStyle:\n+        \"\"\"Get the base style.\"\"\"\n+        style = self.baseStyle()\n+        assert style is not None\n+        return style\n \n     def _draw_indicator(self, layouts, opt, p):\n         \"\"\"Draw the tab indicator.\n@@ -837,7 +839,7 @@ class TabBarStyle(QCommonStyle):\n         icon_state = (QIcon.State.On if opt.state &amp; QStyle.StateFlag.State_Selected\n                       else QIcon.State.Off)\n         icon = opt.icon.pixmap(opt.iconSize, icon_mode, icon_state)\n-        self._style.drawItemPixmap(p, layouts.icon, Qt.AlignmentFlag.AlignCenter, icon)\n+        self._base_style().drawItemPixmap(p, layouts.icon, Qt.AlignmentFlag.AlignCenter, icon)\n \n     def drawControl(self, element, opt, p, widget=None):\n         \"\"\"Override drawControl to draw odd tabs in a different color.\n@@ -854,7 +856,7 @@ class TabBarStyle(QCommonStyle):\n         if element not in [QStyle.ControlElement.CE_TabBarTab, QStyle.ControlElement.CE_TabBarTabShape,\n                            QStyle.ControlElement.CE_TabBarTabLabel]:\n             # Let the real style draw it.\n-            self._style.drawControl(element, opt, p, widget)\n+            self._base_style().drawControl(element, opt, p, widget)\n             return\n \n         layouts = self._tab_layout(opt)\n@@ -869,21 +871,23 @@ class TabBarStyle(QCommonStyle):\n         elif element == QStyle.ControlElement.CE_TabBarTabShape:\n             p.fillRect(opt.rect, opt.palette.window())\n             self._draw_indicator(layouts, opt, p)\n-            # We use super() rather than self._style here because we don't want\n+            # We use QCommonStyle rather than self.baseStyle() here because we don't want\n             # any sophisticated drawing.\n-            super().drawControl(QStyle.ControlElement.CE_TabBarTabShape, opt, p, widget)\n+            QCommonStyle.drawControl(self, QStyle.ControlElement.CE_TabBarTabShape, opt, p, widget)\n         elif element == QStyle.ControlElement.CE_TabBarTabLabel:\n             if not opt.icon.isNull() and layouts.icon.isValid():\n                 self._draw_icon(layouts, opt, p)\n             alignment = (config.cache['tabs.title.alignment'] |\n                          Qt.AlignmentFlag.AlignVCenter | Qt.TextFlag.TextHideMnemonic)\n-            self._style.drawItemText(p,\n-                                     layouts.text,\n-                                     int(alignment),\n-                                     opt.palette,\n-                                     bool(opt.state &amp; QStyle.StateFlag.State_Enabled),\n-                                     opt.text,\n-                                     QPalette.ColorRole.WindowText)\n+            self._base_style().drawItemText(\n+                p,\n+                layouts.text,\n+                int(alignment),\n+                opt.palette,\n+                bool(opt.state &amp; QStyle.StateFlag.State_Enabled),\n+                opt.text,\n+                QPalette.ColorRole.WindowText\n+            )\n         else:\n             raise ValueError(\"Invalid element {!r}\".format(element))\n \n@@ -905,7 +909,7 @@ class TabBarStyle(QCommonStyle):\n                       QStyle.PixelMetric.PM_TabBarScrollButtonWidth]:\n             return 0\n         else:\n-            return self._style.pixelMetric(metric, option, widget)\n+            return self._base_style().pixelMetric(metric, option, widget)\n \n     def subElementRect(self, sr, opt, widget=None):\n         \"\"\"Override subElementRect to use our own _tab_layout implementation.\n@@ -930,12 +934,12 @@ class TabBarStyle(QCommonStyle):\n             # aligned properly. Otherwise, empty space will be shown after the\n             # last tab even though the button width is set to 0\n             #\n-            # Need to use super() because we also use super() to render\n+            # Need to use QCommonStyle here because we also use it to render\n             # element in drawControl(); otherwise, we may get bit by\n             # style differences...\n-            return super().subElementRect(sr, opt, widget)\n+            return QCommonStyle.subElementRect(self, sr, opt, widget)\n         else:\n-            return self._style.subElementRect(sr, opt, widget)\n+            return self._base_style().subElementRect(sr, opt, widget)\n \n     def _tab_layout(self, opt):\n         \"\"\"Compute the text/icon rect from the opt rect.\n@@ -982,7 +986,7 @@ class TabBarStyle(QCommonStyle):\n             text_rect.adjust(\n                 icon_rect.width() + TabBarStyle.ICON_PADDING, 0, 0, 0)\n \n-        text_rect = self._style.visualRect(opt.direction, opt.rect, text_rect)\n+        text_rect = self._base_style().visualRect(opt.direction, opt.rect, text_rect)\n         return Layouts(text=text_rect, icon=icon_rect,\n                        indicator=indicator_rect)\n \n@@ -1017,5 +1021,5 @@ class TabBarStyle(QCommonStyle):\n \n         icon_top = text_rect.center().y() + 1 - tab_icon_size.height() // 2\n         icon_rect = QRect(QPoint(text_rect.left(), icon_top), tab_icon_size)\n-        icon_rect = self._style.visualRect(opt.direction, opt.rect, icon_rect)\n+        icon_rect = self._base_style().visualRect(opt.direction, opt.rect, icon_rect)\n         return icon_rect\ndiff --git a/qutebrowser/mainwindow/windowundo.py b/qutebrowser/mainwindow/windowundo.py\nindex 0104a8187..46ff3c8c5 100644\n--- a/qutebrowser/mainwindow/windowundo.py\n+++ b/qutebrowser/mainwindow/windowundo.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Code for :undo --window.\"\"\"\n \n@@ -83,9 +68,9 @@ class WindowUndoManager(QObject):\n             private=False,\n             geometry=entry.geometry,\n         )\n-        window.show()\n         window.tabbed_browser.undo_stack = entry.tab_stack\n         window.tabbed_browser.undo()\n+        window.show()\n \n \n def init():\ndiff --git a/qutebrowser/misc/__init__.py b/qutebrowser/misc/__init__.py\nindex 8f68e3d6f..8bfe166d4 100644\n--- a/qutebrowser/misc/__init__.py\n+++ b/qutebrowser/misc/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Misc. modules.\"\"\"\ndiff --git a/qutebrowser/misc/autoupdate.py b/qutebrowser/misc/autoupdate.py\nindex ca21181c0..b87e1db3e 100644\n--- a/qutebrowser/misc/autoupdate.py\n+++ b/qutebrowser/misc/autoupdate.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Classes related to auto-updating and getting the latest version.\"\"\"\n \ndiff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py\nindex 819e090f0..51d3a35c3 100644\n--- a/qutebrowser/misc/backendproblem.py\n+++ b/qutebrowser/misc/backendproblem.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Dialogs shown when there was a problem with a backend choice.\"\"\"\n \n@@ -25,10 +10,12 @@ import functools\n import html\n import enum\n import shutil\n+import os.path\n import argparse\n import dataclasses\n-from typing import Any, List, Sequence, Tuple, Optional\n+from typing import Any, Optional, Sequence, Tuple\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import Qt\n from qutebrowser.qt.widgets import (QDialog, QPushButton, QHBoxLayout, QVBoxLayout, QLabel,\n                              QMessageBox, QWidget)\n@@ -71,27 +58,35 @@ def _other_backend(backend: usertypes.Backend) -&gt; Tuple[usertypes.Backend, str]:\n     return (other_backend, other_setting)\n \n \n-def _error_text(because: str, text: str, backend: usertypes.Backend) -&gt; str:\n+def _error_text(\n+    because: str,\n+    text: str,\n+    backend: usertypes.Backend,\n+    suggest_other_backend: bool = False,\n+) -&gt; str:\n     \"\"\"Get an error text for the given information.\"\"\"\n-    other_backend, other_setting = _other_backend(backend)\n-    if other_backend == usertypes.Backend.QtWebKit:\n-        warning = (\"Note that QtWebKit hasn't been updated since \"\n-                   \"July 2017 (including security updates).\")\n-        suffix = \" (not recommended)\"\n-    else:\n-        warning = \"\"\n-        suffix = \"\"\n-    return (\"Failed to start with the {backend} backend!\"\n-            \"\nqutebrowser tried to start with the {backend} backend but \"\n-            \"failed because {because}.{text}\"\n-            \"\nForcing the {other_backend.name} backend{suffix}\"\n-            \"\nThis forces usage of the {other_backend.name} backend by \"\n-            \"setting the backend = '{other_setting}' option \"\n-            \"(if you have a config.py file, you'll need to set \"\n-            \"this manually). {warning}\".format(\n-                backend=backend.name, because=because, text=text,\n-                other_backend=other_backend, other_setting=other_setting,\n-                warning=warning, suffix=suffix))\n+    text = (f\"Failed to start with the {backend.name} backend!\"\n+            f\"\nqutebrowser tried to start with the {backend.name} backend but \"\n+            f\"failed because {because}.{text}\")\n+\n+    if suggest_other_backend:\n+        other_backend, other_setting = _other_backend(backend)\n+        if other_backend == usertypes.Backend.QtWebKit:\n+            warning = (\"Note that QtWebKit hasn't been updated since \"\n+                    \"July 2017 (including security updates).\")\n+            suffix = \" (not recommended)\"\n+        else:\n+            warning = \"\"\n+            suffix = \"\"\n+\n+        text += (f\"\nForcing the {other_backend.name} backend{suffix}\"\n+                 f\"\nThis forces usage of the {other_backend.name} backend by \"\n+                 f\"setting the backend = '{other_setting}' option \"\n+                 f\"(if you have a config.py file, you'll need to set \"\n+                 f\"this manually). {warning}\")\n+\n+    text += f\"\n{machinery.INFO.to_html()}\"\n+    return text\n \n \n class _Dialog(QDialog):\n@@ -101,13 +96,14 @@ class _Dialog(QDialog):\n     def __init__(self, *, because: str,\n                  text: str,\n                  backend: usertypes.Backend,\n+                 suggest_other_backend: bool = True,\n                  buttons: Sequence[_Button] = None,\n                  parent: QWidget = None) -&gt; None:\n         super().__init__(parent)\n         vbox = QVBoxLayout(self)\n \n-        other_backend, other_setting = _other_backend(backend)\n-        text = _error_text(because, text, backend)\n+        text = _error_text(because, text, backend,\n+                           suggest_other_backend=suggest_other_backend)\n \n         label = QLabel(text)\n         label.setWordWrap(True)\n@@ -121,13 +117,15 @@ class _Dialog(QDialog):\n         quit_button.clicked.connect(lambda: self.done(_Result.quit))\n         hbox.addWidget(quit_button)\n \n-        backend_text = \"Force {} backend\".format(other_backend.name)\n-        if other_backend == usertypes.Backend.QtWebKit:\n-            backend_text += ' (not recommended)'\n-        backend_button = QPushButton(backend_text)\n-        backend_button.clicked.connect(functools.partial(\n-            self._change_setting, 'backend', other_setting))\n-        hbox.addWidget(backend_button)\n+        if suggest_other_backend:\n+            other_backend, other_setting = _other_backend(backend)\n+            backend_text = \"Force {} backend\".format(other_backend.name)\n+            if other_backend == usertypes.Backend.QtWebKit:\n+                backend_text += ' (not recommended)'\n+            backend_button = QPushButton(backend_text)\n+            backend_button.clicked.connect(functools.partial(\n+                self._change_setting, 'backend', other_setting))\n+            hbox.addWidget(backend_button)\n \n         for button in buttons:\n             btn = QPushButton(button.text)\n@@ -194,76 +192,6 @@ class _BackendProblemChecker:\n \n         sys.exit(usertypes.Exit.err_init)\n \n-    def _xwayland_options(self) -&gt; Tuple[str, List[_Button]]:\n-        \"\"\"Get buttons/text for a possible XWayland solution.\"\"\"\n-        buttons = []\n-        text = \"\nYou can work around this in one of the following ways:\"\n-\n-        if 'DISPLAY' in os.environ:\n-            # XWayland is available, but QT_QPA_PLATFORM=wayland is set\n-            buttons.append(\n-                _Button(\"Force XWayland\", 'qt.force_platform', 'xcb'))\n-            text += (\"\nForce Qt to use XWayland\"\n-                     \"\nThis allows you to use the newer QtWebEngine backend \"\n-                     \"(based on Chromium). \"\n-                     \"This sets the qt.force_platform = 'xcb' option \"\n-                     \"(if you have a config.py file, you'll need to \"\n-                     \"set this manually).\")\n-        else:\n-            text += (\"\nSet up XWayland\"\n-                     \"\nThis allows you to use the newer QtWebEngine backend \"\n-                     \"(based on Chromium). \")\n-\n-        return text, buttons\n-\n-    def _handle_wayland_webgl(self) -&gt; None:\n-        \"\"\"On older graphic hardware, WebGL on Wayland causes segfaults.\n-\n-        See https://github.com/qutebrowser/qutebrowser/issues/5313\n-        \"\"\"\n-        self._assert_backend(usertypes.Backend.QtWebEngine)\n-\n-        if os.environ.get('QUTE_SKIP_WAYLAND_WEBGL_CHECK'):\n-            return\n-\n-        platform = objects.qapp.platformName()\n-        if platform not in ['wayland', 'wayland-egl']:\n-            return\n-\n-        # Only Qt 5.14 should be affected\n-        if not qtutils.version_check('5.14', compiled=False):\n-            return\n-        if qtutils.version_check('5.15', compiled=False):\n-            return\n-\n-        # Newer graphic hardware isn't affected\n-        opengl_info = version.opengl_info()\n-        if (opengl_info is None or\n-                opengl_info.gles or\n-                opengl_info.version is None or\n-                opengl_info.version &gt;= (4, 3)):\n-            return\n-\n-        # If WebGL is turned off, we're fine\n-        if not config.val.content.webgl:\n-            return\n-\n-        text, buttons = self._xwayland_options()\n-\n-        buttons.append(_Button(\"Turn off WebGL (recommended)\",\n-                               'content.webgl',\n-                               False))\n-        text += (\"\nDisable WebGL (recommended)\"\n-                 \"This sets the content.webgl = False option \"\n-                 \"(if you have a config.py file, you'll need to \"\n-                 \"set this manually).\")\n-\n-        self._show_dialog(backend=usertypes.Backend.QtWebEngine,\n-                          because=(\"of frequent crashes with Qt 5.14 on \"\n-                                   \"Wayland with older graphics hardware\"),\n-                          text=text,\n-                          buttons=buttons)\n-\n     def _try_import_backends(self) -&gt; _BackendImports:\n         \"\"\"Check whether backends can be imported and return BackendImports.\"\"\"\n         # pylint: disable=unused-import\n@@ -273,6 +201,7 @@ class _BackendProblemChecker:\n             from qutebrowser.qt import webkit, webkitwidgets\n         except (ImportError, ValueError) as e:\n             results.webkit_error = str(e)\n+            assert results.webkit_error\n         else:\n             if not qtutils.is_new_qtwebkit():\n                 results.webkit_error = \"Unsupported legacy QtWebKit found\"\n@@ -281,6 +210,7 @@ class _BackendProblemChecker:\n             from qutebrowser.qt import webenginecore, webenginewidgets\n         except (ImportError, ValueError) as e:\n             results.webengine_error = str(e)\n+            assert results.webengine_error\n \n         return results\n \n@@ -292,19 +222,8 @@ class _BackendProblemChecker:\n         if QSslSocket.supportsSsl():\n             return\n \n-        if qtutils.version_check('5.12.4'):\n-            version_text = (\"If you use OpenSSL 1.0 with a PyQt package from \"\n-                            \"PyPI (e.g. on Ubuntu 16.04), you will need to \"\n-                            \"build OpenSSL 1.1 from sources and set \"\n-                            \"LD_LIBRARY_PATH accordingly.\")\n-        else:\n-            version_text = (\"If you use OpenSSL 1.1 with a PyQt package from \"\n-                            \"PyPI (e.g. on Archlinux or Debian Stretch), you \"\n-                            \"need to set LD_LIBRARY_PATH to the path of \"\n-                            \"OpenSSL 1.0 or use Qt &gt;= 5.12.4.\")\n-\n-        text = (\"Could not initialize QtNetwork SSL support. {} This only \"\n-                \"affects downloads and :adblock-update.\".format(version_text))\n+        text = (\"Could not initialize QtNetwork SSL support. This only \"\n+                \"affects downloads and :adblock-update.\")\n \n         if fatal:\n             errbox = msgbox.msgbox(parent=None,\n@@ -315,6 +234,13 @@ class _BackendProblemChecker:\n             errbox.exec()\n             sys.exit(usertypes.Exit.err_init)\n \n+        # Doing this here because it's not relevant with QtWebKit where fatal=True\n+        if machinery.IS_QT6:\n+            text += (\"\\nHint: If installed via mkvenv.py on a system without \"\n+                     \"OpenSSL 3.x (e.g. Ubuntu 20.04), you can use --pyqt-version 6.4 \"\n+                     \"to get an older Qt still compatible with OpenSSL 1.1 (at the \"\n+                     \"expense of running an older QtWebEngine/Chromium)\")\n+\n         assert not fatal\n         log.init.warning(text)\n \n@@ -330,9 +256,11 @@ class _BackendProblemChecker:\n                     \"\nThe errors encountered were:\n\"\n                     \"\nQtWebKit: {webkit_error}\"\n                     \"\nQtWebEngine: {webengine_error}\"\n-                    \"\".format(\n+                    \"\n{info}\".format(\n                         webkit_error=html.escape(imports.webkit_error),\n-                        webengine_error=html.escape(imports.webengine_error)))\n+                        webengine_error=html.escape(imports.webengine_error),\n+                        info=machinery.INFO.to_html(),\n+                    ))\n             errbox = msgbox.msgbox(parent=None,\n                                    title=\"No backend library found!\",\n                                    text=text,\n@@ -361,26 +289,6 @@ class _BackendProblemChecker:\n \n         raise utils.Unreachable\n \n-    def _handle_cache_nuking(self) -&gt; None:\n-        \"\"\"Nuke the QtWebEngine cache if the Qt version changed.\n-\n-        WORKAROUND for https://bugreports.qt.io/browse/QTBUG-72532\n-        \"\"\"\n-        if not configfiles.state.qt_version_changed:\n-            return\n-\n-        # Only nuke the cache in cases where we know there are problems.\n-        # It seems these issues started with Qt 5.12.\n-        # They should be fixed with Qt 5.12.5:\n-        # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/265408\n-        if qtutils.version_check('5.12.5', compiled=False):\n-            return\n-\n-        log.init.info(\"Qt version changed, nuking QtWebEngine cache\")\n-        cache_dir = os.path.join(standarddir.cache(), 'webengine')\n-        if os.path.exists(cache_dir):\n-            shutil.rmtree(cache_dir)\n-\n     def _handle_serviceworker_nuking(self) -&gt; None:\n         \"\"\"Nuke the service workers directory if the Qt version changed.\n \n@@ -389,13 +297,7 @@ class _BackendProblemChecker:\n         https://bugreports.qt.io/browse/QTBUG-82105\n         https://bugreports.qt.io/browse/QTBUG-93744\n         \"\"\"\n-        if ('serviceworker_workaround' not in configfiles.state['general'] and\n-                qtutils.version_check('5.14', compiled=False)):\n-            # Nuke the service worker directory once for every install with Qt\n-            # 5.14, given that it seems to cause a variety of segfaults.\n-            configfiles.state['general']['serviceworker_workaround'] = '514'\n-            reason = 'Qt 5.14'\n-        elif configfiles.state.qt_version_changed:\n+        if configfiles.state.qt_version_changed:\n             reason = 'Qt version changed'\n         elif configfiles.state.qtwe_version_changed:\n             reason = 'QtWebEngine version changed'\n@@ -420,6 +322,105 @@ class _BackendProblemChecker:\n \n         shutil.move(service_worker_dir, bak_dir)\n \n+    def _confirm_chromium_version_changes(self) -&gt; None:\n+        \"\"\"Ask if there are Chromium downgrades or a Qt 5 -&gt; 6 upgrade.\"\"\"\n+        versions = version.qtwebengine_versions(avoid_init=True)\n+        change = configfiles.state.chromium_version_changed\n+        info = f\"{machinery.INFO.to_html()}\"\n+        if machinery.INFO.reason == machinery.SelectionReason.auto:\n+            info += (\n+                \"\"\n+                \"You can use --qt-wrapper or set QUTE_QT_WRAPPER \"\n+                \"in your environment to override this.\"\n+            )\n+        webengine_data_dir = os.path.join(standarddir.data(), \"webengine\")\n+\n+        if change == configfiles.VersionChange.major:\n+            icon = QMessageBox.Icon.Information\n+            text = (\n+                \"Chromium/QtWebEngine upgrade detected:\"\n+                f\"You are upgrading to QtWebEngine {versions.webengine} but \"\n+                \"used Qt 5 for the last qutebrowser launch.\"\n+                \"Data managed by Chromium will be upgraded. This is a one-way \"\n+                \"operation: If you open qutebrowser with Qt 5 again later, any \"\n+                \"Chromium data will be invalid and discarded.\"\n+                \"This affects page data such as cookies, but not data managed by \"\n+                \"qutebrowser, such as your configuration or :open history.\"\n+                f\"The affected data is in {webengine_data_dir}.\"\n+            ) + info\n+        elif change == configfiles.VersionChange.downgrade:\n+            icon = QMessageBox.Icon.Warning\n+            text = (\n+                \"Chromium/QtWebEngine downgrade detected:\"\n+                f\"You are downgrading to QtWebEngine {versions.webengine}.\"\n+                \"\"\n+                \"Data managed by Chromium will be discarded if you continue.\"\n+                \"\"\n+                \"This affects page data such as cookies, but not data managed by \"\n+                \"qutebrowser, such as your configuration or :open history.\"\n+                f\"The affected data is in {webengine_data_dir}.\"\n+            ) + info\n+        else:\n+            return\n+\n+        box = msgbox.msgbox(\n+            parent=None,\n+            title=\"QtWebEngine version change\",\n+            text=text,\n+            icon=icon,\n+            plain_text=False,\n+            buttons=QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Abort,\n+        )\n+        response = box.exec()\n+        if response != QMessageBox.StandardButton.Ok:\n+            sys.exit(usertypes.Exit.err_init)\n+\n+    def _check_webengine_version(self) -&gt; None:\n+        versions = version.qtwebengine_versions(avoid_init=True)\n+        if versions.webengine &lt; utils.VersionNumber(5, 15, 2):\n+            text = (\n+                \"QtWebEngine &gt;= 5.15.2 is required for qutebrowser, but \"\n+                f\"{versions.webengine} is installed.\")\n+            errbox = msgbox.msgbox(parent=None,\n+                                   title=\"QtWebEngine too old\",\n+                                   text=text,\n+                                   icon=QMessageBox.Icon.Critical,\n+                                   plain_text=False)\n+            errbox.exec()\n+            sys.exit(usertypes.Exit.err_init)\n+\n+    def _check_software_rendering(self) -&gt; None:\n+        \"\"\"Avoid crashing software rendering settings.\n+\n+        WORKAROUND for https://bugreports.qt.io/browse/QTBUG-103372\n+        Fixed with QtWebEngine 6.3.1.\n+        \"\"\"\n+        self._assert_backend(usertypes.Backend.QtWebEngine)\n+        versions = version.qtwebengine_versions(avoid_init=True)\n+\n+        if versions.webengine != utils.VersionNumber(6, 3):\n+            return\n+\n+        if os.environ.get('QT_QUICK_BACKEND') != 'software':\n+            return\n+\n+        text = (\"You can instead force software rendering on the Chromium level (sets \"\n+                \"qt.force_software_rendering to chromium instead of \"\n+                \"qt-quick).\")\n+\n+        button = _Button(\"Force Chromium software rendering\",\n+                         'qt.force_software_rendering',\n+                         'chromium')\n+        self._show_dialog(\n+            backend=usertypes.Backend.QtWebEngine,\n+            suggest_other_backend=False,\n+            because=\"a Qt 6.3.0 bug causes instant crashes with Qt Quick software rendering\",\n+            text=text,\n+            buttons=[button],\n+        )\n+\n+        raise utils.Unreachable\n+\n     def _assert_backend(self, backend: usertypes.Backend) -&gt; None:\n         assert objects.backend == backend, objects.backend\n \n@@ -427,10 +428,11 @@ class _BackendProblemChecker:\n         \"\"\"Run all checks.\"\"\"\n         self._check_backend_modules()\n         if objects.backend == usertypes.Backend.QtWebEngine:\n+            self._check_webengine_version()\n             self._handle_ssl_support()\n-            self._handle_wayland_webgl()\n-            self._handle_cache_nuking()\n             self._handle_serviceworker_nuking()\n+            self._check_software_rendering()\n+            self._confirm_chromium_version_changes()\n         else:\n             self._assert_backend(usertypes.Backend.QtWebKit)\n             self._handle_ssl_support(fatal=True)\ndiff --git a/qutebrowser/misc/binparsing.py b/qutebrowser/misc/binparsing.py\nnew file mode 100644\nindex 000000000..81e2e6dbb\n--- /dev/null\n+++ b/qutebrowser/misc/binparsing.py\n@@ -0,0 +1,43 @@\n+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n+\"\"\"Utilities for parsing binary files.\n+\n+Used by elf.py as well as pakjoy.py.\n+\"\"\"\n+\n+import struct\n+from typing import Any, IO, Tuple\n+\n+\n+class ParseError(Exception):\n+\n+    \"\"\"Raised when the file can't be parsed.\"\"\"\n+\n+\n+def unpack(fmt: str, fobj: IO[bytes]) -&gt; Tuple[Any, ...]:\n+    \"\"\"Unpack the given struct format from the given file.\"\"\"\n+    size = struct.calcsize(fmt)\n+    data = safe_read(fobj, size)\n+\n+    try:\n+        return struct.unpack(fmt, data)\n+    except struct.error as e:\n+        raise ParseError(e)\n+\n+\n+def safe_read(fobj: IO[bytes], size: int) -&gt; bytes:\n+    \"\"\"Read from a file, handling possible exceptions.\"\"\"\n+    try:\n+        return fobj.read(size)\n+    except (OSError, OverflowError) as e:\n+        raise ParseError(e)\n+\n+\n+def safe_seek(fobj: IO[bytes], pos: int) -&gt; None:\n+    \"\"\"Seek in a file, handling possible exceptions.\"\"\"\n+    try:\n+        fobj.seek(pos)\n+    except (OSError, OverflowError) as e:\n+        raise ParseError(e)\ndiff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py\nindex 7d6a524c3..596a7803a 100644\n--- a/qutebrowser/misc/checkpyver.py\n+++ b/qutebrowser/misc/checkpyver.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The-Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Check if qutebrowser is run with the correct python version.\n \n@@ -30,8 +15,8 @@ try:\n except ImportError:  # pragma: no cover\n     try:\n         # Python2\n-        from Tkinter import Tk  # type: ignore[import, no-redef]\n-        import tkMessageBox as messagebox  # type: ignore[import, no-redef] # noqa: N813\n+        from Tkinter import Tk  # type: ignore[import-not-found, no-redef]\n+        import tkMessageBox as messagebox  # type: ignore[import-not-found, no-redef] # noqa: N813\n     except ImportError:\n         # Some Python without Tk\n         Tk = None  # type: ignore[misc, assignment]\n@@ -43,13 +28,15 @@ except ImportError:  # pragma: no cover\n # to stderr.\n def check_python_version():\n     \"\"\"Check if correct python version is run.\"\"\"\n-    if sys.hexversion &lt; 0x03070000:\n+    if sys.hexversion &lt; 0x03080000:\n         # We don't use .format() and print_function here just in case someone\n         # still has &lt; 2.6 installed.\n         version_str = '.'.join(map(str, sys.version_info[:3]))\n-        text = (\"At least Python 3.7 is required to run qutebrowser, but \" +\n+        text = (\"At least Python 3.8 is required to run qutebrowser, but \" +\n                 \"it's running with \" + version_str + \".\\n\")\n-        if Tk and '--no-err-windows' not in sys.argv:  # pragma: no cover\n+\n+        show_errors = '--no-err-windows' not in sys.argv\n+        if Tk and show_errors:  # type: ignore[truthy-function]  # pragma: no cover\n             root = Tk()\n             root.withdraw()\n             messagebox.showerror(\"qutebrowser: Fatal error!\", text)\ndiff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py\nindex cd880a0fc..aa2df63e0 100644\n--- a/qutebrowser/misc/cmdhistory.py\n+++ b/qutebrowser/misc/cmdhistory.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Command history for the status bar.\"\"\"\n \ndiff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py\nindex b2c2b4571..08f5dc5ff 100644\n--- a/qutebrowser/misc/consolewidget.py\n+++ b/qutebrowser/misc/consolewidget.py\n@@ -1,27 +1,12 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Debugging console.\"\"\"\n \n import sys\n import code\n-from typing import MutableSequence\n+from typing import MutableSequence, Optional\n \n from qutebrowser.qt.core import pyqtSignal, pyqtSlot, Qt\n from qutebrowser.qt.widgets import QTextEdit, QWidget, QVBoxLayout, QApplication\n@@ -32,7 +17,7 @@ from qutebrowser.misc import cmdhistory, miscwidgets\n from qutebrowser.utils import utils, objreg\n \n \n-console_widget = None\n+console_widget: Optional[\"ConsoleWidget\"] = None\n \n \n class ConsoleLineEdit(miscwidgets.CommandLineEdit):\n@@ -127,6 +112,7 @@ class ConsoleTextEdit(QTextEdit):\n         self.moveCursor(QTextCursor.MoveOperation.End)\n         self.insertPlainText(text)\n         scrollbar = self.verticalScrollBar()\n+        assert scrollbar is not None\n         scrollbar.setValue(scrollbar.maximum())\n \n \ndiff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py\nindex b3998cf27..ad9ce83a7 100644\n--- a/qutebrowser/misc/crashdialog.py\n+++ b/qutebrowser/misc/crashdialog.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The dialog which gets shown when qutebrowser crashes.\"\"\"\n \n@@ -512,7 +497,7 @@ class FatalCrashDialog(_CrashDialog):\n                 \"Note: Crash reports for fatal crashes sometimes don't \"\n                 \"contain the information necessary to fix an issue. Please \"\n                 \"follow the steps in \"\n+                \"qutebrowser/blob/main/doc/stacktrace.asciidoc'&gt;\"\n                 \"stacktrace.asciidoc to submit a stacktrace.\")\n         self._lbl.setText(text)\n \ndiff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py\nindex a0144e26d..05e5806df 100644\n--- a/qutebrowser/misc/crashsignal.py\n+++ b/qutebrowser/misc/crashsignal.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Handlers for crashes and OS signals.\"\"\"\n \n@@ -37,8 +22,9 @@ from qutebrowser.qt.core import (pyqtSlot, qInstallMessageHandler, QObject,\n from qutebrowser.qt.widgets import QApplication\n \n from qutebrowser.api import cmdutils\n+from qutebrowser.config import configfiles, configexc\n from qutebrowser.misc import earlyinit, crashdialog, ipc, objects\n-from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils\n+from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils, message\n from qutebrowser.qt import sip\n if TYPE_CHECKING:\n     from qutebrowser.misc import quitter\n@@ -337,6 +323,17 @@ class SignalHandler(QObject):\n         self._activated = False\n         self._orig_wakeup_fd: Optional[int] = None\n \n+        self._handlers = {\n+            signal.SIGINT: self.interrupt,\n+            signal.SIGTERM: self.interrupt,\n+        }\n+        platform_dependant_handlers = {\n+            \"SIGHUP\": self.reload_config,\n+        }\n+        for sig_str, handler in platform_dependant_handlers.items():\n+            if hasattr(signal.Signals, sig_str):\n+                self._handlers[signal.Signals[sig_str]] = handler\n+\n     def activate(self):\n         \"\"\"Set up signal handlers.\n \n@@ -346,10 +343,8 @@ class SignalHandler(QObject):\n         On Unix, it uses a QSocketNotifier with os.set_wakeup_fd to get\n         notified.\n         \"\"\"\n-        self._orig_handlers[signal.SIGINT] = signal.signal(\n-            signal.SIGINT, self.interrupt)\n-        self._orig_handlers[signal.SIGTERM] = signal.signal(\n-            signal.SIGTERM, self.interrupt)\n+        for sig, handler in self._handlers.items():\n+            self._orig_handlers[sig] = signal.signal(sig, handler)\n \n         if utils.is_posix and hasattr(signal, 'set_wakeup_fd'):\n             # pylint: disable=import-error,no-member,useless-suppression\n@@ -445,6 +440,15 @@ class SignalHandler(QObject):\n         print(\"WHY ARE YOU DOING THIS TO ME? :(\")\n         sys.exit(128 + signum)\n \n+    def reload_config(self, _signum, _frame):\n+        \"\"\"Reload the config.\"\"\"\n+        log.signals.info(\"SIGHUP received, reloading config.\")\n+        filename = standarddir.config_py()\n+        try:\n+            configfiles.read_config_py(filename)\n+        except configexc.ConfigFileErrors as e:\n+            message.error(str(e))\n+\n \n def init(q_app: QApplication,\n          args: argparse.Namespace,\ndiff --git a/qutebrowser/misc/debugcachestats.py b/qutebrowser/misc/debugcachestats.py\nindex 9090bd0ea..d3ac9819b 100644\n--- a/qutebrowser/misc/debugcachestats.py\n+++ b/qutebrowser/misc/debugcachestats.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2019-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Implementation of the command debug-cache-stats.\n \ndiff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py\nindex d8719235d..a7bdb8252 100644\n--- a/qutebrowser/misc/earlyinit.py\n+++ b/qutebrowser/misc/earlyinit.py\n@@ -1,25 +1,10 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The-Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Things which need to be done really early (e.g. before importing Qt).\n \n-At this point we can be sure we have all python 3.7 features available.\n+At this point we can be sure we have all python 3.8 features available.\n \"\"\"\n \n try:\n@@ -35,6 +20,7 @@ import traceback\n import signal\n import importlib\n import datetime\n+from typing import NoReturn\n try:\n     import tkinter\n except ImportError:\n@@ -42,6 +28,10 @@ except ImportError:\n \n # NOTE: No qutebrowser or PyQt import should be done here, as some early\n # initialization needs to take place before that!\n+#\n+# The machinery module is an exception, as it also is required to never import Qt\n+# itself at import time.\n+from qutebrowser.qt import machinery\n \n \n START_TIME = datetime.datetime.now()\n@@ -59,7 +49,7 @@ def _missing_str(name, *, webengine=False):\n               \"The error encountered was:%ERROR%\"]\n     lines = ['Please search for the python3 version of {} in your '\n              'distributions packages, or see '\n-             'https://github.com/qutebrowser/qutebrowser/blob/master/doc/install.asciidoc'\n+             'https://github.com/qutebrowser/qutebrowser/blob/main/doc/install.asciidoc'\n              .format(name)]\n     blocks.append(''.join(lines))\n     if not webengine:\n@@ -136,11 +126,26 @@ def init_faulthandler(fileobj=sys.__stderr__):\n         # pylint: enable=no-member,useless-suppression\n \n \n-def check_pyqt():\n-    \"\"\"Check if PyQt core modules (QtCore/QtWidgets) are installed.\"\"\"\n-    from qutebrowser.qt import machinery\n+def _fatal_qt_error(text: str) -&gt; NoReturn:\n+    \"\"\"Show a fatal error about Qt being missing.\"\"\"\n+    if tkinter and '--no-err-windows' not in sys.argv:\n+        root = tkinter.Tk()\n+        root.withdraw()\n+        tkinter.messagebox.showerror(\"qutebrowser: Fatal error!\", text)\n+    else:\n+        print(text, file=sys.stderr)\n+    if '--debug' in sys.argv or '--no-err-windows' in sys.argv:\n+        print(file=sys.stderr)\n+        traceback.print_exc()\n+    sys.exit(1)\n+\n \n-    packages = [f'{machinery.PACKAGE}.QtCore', f'{machinery.PACKAGE}.QtWidgets']\n+def check_qt_available(info: machinery.SelectionInfo) -&gt; None:\n+    \"\"\"Check if Qt core modules (QtCore/QtWidgets) are installed.\"\"\"\n+    if info.wrapper is None:\n+        _fatal_qt_error(f\"No Qt wrapper was importable.\\n\\n{info}\")\n+\n+    packages = [f'{info.wrapper}.QtCore', f'{info.wrapper}.QtWidgets']\n     for name in packages:\n         try:\n             importlib.import_module(name)\n@@ -150,16 +155,8 @@ def check_pyqt():\n             text = text.replace('', '')\n             text = text.replace('', '\\n')\n             text = text.replace('%ERROR%', str(e))\n-            if tkinter and '--no-err-windows' not in sys.argv:\n-                root = tkinter.Tk()\n-                root.withdraw()\n-                tkinter.messagebox.showerror(\"qutebrowser: Fatal error!\", text)\n-            else:\n-                print(text, file=sys.stderr)\n-            if '--debug' in sys.argv or '--no-err-windows' in sys.argv:\n-                print(file=sys.stderr)\n-                traceback.print_exc()\n-            sys.exit(1)\n+            text += '\\n\\n' + str(info)\n+            _fatal_qt_error(text)\n \n \n def qt_version(qversion=None, qt_version_str=None):\n@@ -177,27 +174,32 @@ def qt_version(qversion=None, qt_version_str=None):\n         return qversion\n \n \n+def get_qt_version():\n+    \"\"\"Get the Qt version, or None if too old for QLibaryInfo.version().\"\"\"\n+    try:\n+        from qutebrowser.qt.core import QLibraryInfo\n+        return QLibraryInfo.version().normalized()\n+    except (ImportError, AttributeError):\n+        return None\n+\n+\n def check_qt_version():\n     \"\"\"Check if the Qt version is recent enough.\"\"\"\n     from qutebrowser.qt.core import QT_VERSION, PYQT_VERSION, PYQT_VERSION_STR\n-    try:\n-        from qutebrowser.qt.core import QVersionNumber, QLibraryInfo\n-        qt_ver = QLibraryInfo.version().normalized()\n-        recent_qt_runtime = qt_ver &gt;= QVersionNumber(5, 12)  # type: ignore[operator]\n-    except (ImportError, AttributeError):\n-        # QVersionNumber was added in Qt 5.6, QLibraryInfo.version() in 5.8\n-        recent_qt_runtime = False\n+    from qutebrowser.qt.core import QVersionNumber\n+    qt_ver = get_qt_version()\n+    recent_qt_runtime = qt_ver is not None and qt_ver &gt;= QVersionNumber(5, 15)\n \n-    if QT_VERSION &lt; 0x050C00 or PYQT_VERSION &lt; 0x050C00 or not recent_qt_runtime:\n-        text = (\"Fatal error: Qt &gt;= 5.12.0 and PyQt &gt;= 5.12.0 are required, \"\n+    if QT_VERSION &lt; 0x050F00 or PYQT_VERSION &lt; 0x050F00 or not recent_qt_runtime:\n+        text = (\"Fatal error: Qt &gt;= 5.15.0 and PyQt &gt;= 5.15.0 are required, \"\n                 \"but Qt {} / PyQt {} is installed.\".format(qt_version(),\n                                                            PYQT_VERSION_STR))\n         _die(text)\n \n-    if qt_ver == QVersionNumber(5, 12, 0):\n-        from qutebrowser.utils import log\n-        log.init.warning(\"Running on Qt 5.12.0. Doing so is unsupported \"\n-                         \"(newer 5.12.x versions are fine).\")\n+    if 0x060000 &lt;= PYQT_VERSION &lt; 0x060202:\n+        text = (\"Fatal error: With Qt 6, PyQt &gt;= 6.2.2 is required, but \"\n+                \"{} is installed.\".format(PYQT_VERSION_STR))\n+        _die(text)\n \n \n def check_ssl_support():\n@@ -235,21 +237,27 @@ def _check_modules(modules):\n \n def check_libraries():\n     \"\"\"Check if all needed Python libraries are installed.\"\"\"\n-    from qutebrowser.qt import machinery\n     modules = {\n         'jinja2': _missing_str(\"jinja2\"),\n         'yaml': _missing_str(\"PyYAML\"),\n     }\n+\n     for subpkg in ['QtQml', 'QtOpenGL', 'QtDBus']:\n-        package = f'{machinery.PACKAGE}.{subpkg}'\n+        package = f'{machinery.INFO.wrapper}.{subpkg}'\n         modules[package] = _missing_str(package)\n+\n     if sys.version_info &lt; (3, 9):\n         # Backport required\n         modules['importlib_resources'] = _missing_str(\"importlib_resources\")\n+\n     if sys.platform.startswith('darwin'):\n-        # Used for resizable hide_decoration windows on macOS\n-        modules['objc'] = _missing_str(\"pyobjc-core\")\n-        modules['AppKit'] = _missing_str(\"pyobjc-framework-Cocoa\")\n+        from qutebrowser.qt.core import QVersionNumber\n+        qt_ver = get_qt_version()\n+        if qt_ver is not None and qt_ver &lt; QVersionNumber(6, 3):\n+            # Used for resizable hide_decoration windows on macOS\n+            modules['objc'] = _missing_str(\"pyobjc-core\")\n+            modules['AppKit'] = _missing_str(\"pyobjc-framework-Cocoa\")\n+\n     _check_modules(modules)\n \n \n@@ -259,21 +267,13 @@ def configure_pyqt():\n     Doing this means we can't use the interactive shell anymore (which we don't\n     anyways), but we can use pdb instead.\n     \"\"\"\n-    from qutebrowser.qt import core as QtCore\n-    QtCore.pyqtRemoveInputHook()\n-    try:\n-        QtCore.pyqt5_enable_new_onexit_scheme(True)  # type: ignore[attr-defined]\n-    except AttributeError:\n-        # Added in PyQt 5.13 somewhere, going to be the default in 5.14\n-        pass\n+    from qutebrowser.qt.core import pyqtRemoveInputHook\n+    pyqtRemoveInputHook()\n \n     from qutebrowser.qt import sip\n-    try:\n-        sip.enableoverflowchecking(True)\n-    except AttributeError:\n+    if machinery.IS_QT5:\n         # default in PyQt6\n-        # FIXME:qt6 solve this in qutebrowser/qt/sip.py equivalent\n-        pass\n+        sip.enableoverflowchecking(True)\n \n \n def init_log(args):\n@@ -287,6 +287,17 @@ def init_log(args):\n     log.init.debug(\"Log initialized.\")\n \n \n+def init_qtlog(args):\n+    \"\"\"Initialize Qt logging.\n+\n+    Args:\n+        args: The argparse namespace.\n+    \"\"\"\n+    from qutebrowser.utils import log, qtlog\n+    qtlog.init(args)\n+    log.init.debug(\"Qt log initialized.\")\n+\n+\n def check_optimize_flag():\n     \"\"\"Check whether qutebrowser is running with -OO.\"\"\"\n     from qutebrowser.utils import log\n@@ -319,14 +330,18 @@ def early_init(args):\n     Args:\n         args: The argparse namespace.\n     \"\"\"\n+    # Init logging as early as possible\n+    init_log(args)\n     # First we initialize the faulthandler as early as possible, so we\n     # theoretically could catch segfaults occurring later during earlyinit.\n     init_faulthandler()\n+    # Then we configure the selected Qt wrapper\n+    info = machinery.init(args)\n+    # Init Qt logging after machinery is initialized\n+    init_qtlog(args)\n     # Here we check if QtCore is available, and if not, print a message to the\n     # console or via Tk.\n-    check_pyqt()\n-    # Init logging as early as possible\n-    init_log(args)\n+    check_qt_available(info)\n     # Now we can be sure QtCore is available, so we can print dialogs on\n     # errors, so people only using the GUI notice them as well.\n     check_libraries()\ndiff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py\nindex 1c4c6ca03..948b4ab9e 100644\n--- a/qutebrowser/misc/editor.py\n+++ b/qutebrowser/misc/editor.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Launcher for an external editor.\"\"\"\n \ndiff --git a/qutebrowser/misc/elf.py b/qutebrowser/misc/elf.py\nindex ea2ef9f37..e44d8b573 100644\n--- a/qutebrowser/misc/elf.py\n+++ b/qutebrowser/misc/elf.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2021 Florian Bruhin (The-Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Simplistic ELF parser to get the QtWebEngine/Chromium versions.\n \n@@ -59,21 +44,16 @@ This is a \"best effort\" parser. If it errors out, we instead end up relying on t\n PyQtWebEngine version, which is the next best thing.\n \"\"\"\n \n-import struct\n import enum\n import re\n import dataclasses\n import mmap\n import pathlib\n-from typing import Any, IO, ClassVar, Dict, Optional, Tuple, cast\n+from typing import IO, ClassVar, Dict, Optional, cast\n \n from qutebrowser.qt import machinery\n from qutebrowser.utils import log, version, qtutils\n-\n-\n-class ParseError(Exception):\n-\n-    \"\"\"Raised when the ELF file can't be parsed.\"\"\"\n+from qutebrowser.misc import binparsing\n \n \n class Bitness(enum.Enum):\n@@ -92,33 +72,6 @@ class Endianness(enum.Enum):\n     big = 2\n \n \n-def _unpack(fmt: str, fobj: IO[bytes]) -&gt; Tuple[Any, ...]:\n-    \"\"\"Unpack the given struct format from the given file.\"\"\"\n-    size = struct.calcsize(fmt)\n-    data = _safe_read(fobj, size)\n-\n-    try:\n-        return struct.unpack(fmt, data)\n-    except struct.error as e:\n-        raise ParseError(e)\n-\n-\n-def _safe_read(fobj: IO[bytes], size: int) -&gt; bytes:\n-    \"\"\"Read from a file, handling possible exceptions.\"\"\"\n-    try:\n-        return fobj.read(size)\n-    except (OSError, OverflowError) as e:\n-        raise ParseError(e)\n-\n-\n-def _safe_seek(fobj: IO[bytes], pos: int) -&gt; None:\n-    \"\"\"Seek in a file, handling possible exceptions.\"\"\"\n-    try:\n-        fobj.seek(pos)\n-    except (OSError, OverflowError) as e:\n-        raise ParseError(e)\n-\n-\n @dataclasses.dataclass\n class Ident:\n \n@@ -140,17 +93,17 @@ class Ident:\n     @classmethod\n     def parse(cls, fobj: IO[bytes]) -&gt; 'Ident':\n         \"\"\"Parse an ELF ident header from a file.\"\"\"\n-        magic, klass, data, elfversion, osabi, abiversion = _unpack(cls._FORMAT, fobj)\n+        magic, klass, data, elfversion, osabi, abiversion = binparsing.unpack(cls._FORMAT, fobj)\n \n         try:\n             bitness = Bitness(klass)\n         except ValueError:\n-            raise ParseError(f\"Invalid bitness {klass}\")\n+            raise binparsing.ParseError(f\"Invalid bitness {klass}\")\n \n         try:\n             endianness = Endianness(data)\n         except ValueError:\n-            raise ParseError(f\"Invalid endianness {data}\")\n+            raise binparsing.ParseError(f\"Invalid endianness {data}\")\n \n         return cls(magic, bitness, endianness, elfversion, osabi, abiversion)\n \n@@ -187,7 +140,7 @@ class Header:\n     def parse(cls, fobj: IO[bytes], bitness: Bitness) -&gt; 'Header':\n         \"\"\"Parse an ELF header from a file.\"\"\"\n         fmt = cls._FORMATS[bitness]\n-        return cls(*_unpack(fmt, fobj))\n+        return cls(*binparsing.unpack(fmt, fobj))\n \n \n @dataclasses.dataclass\n@@ -218,39 +171,39 @@ class SectionHeader:\n     def parse(cls, fobj: IO[bytes], bitness: Bitness) -&gt; 'SectionHeader':\n         \"\"\"Parse an ELF section header from a file.\"\"\"\n         fmt = cls._FORMATS[bitness]\n-        return cls(*_unpack(fmt, fobj))\n+        return cls(*binparsing.unpack(fmt, fobj))\n \n \n def get_rodata_header(f: IO[bytes]) -&gt; SectionHeader:\n     \"\"\"Parse an ELF file and find the .rodata section header.\"\"\"\n     ident = Ident.parse(f)\n     if ident.magic != b'\\x7fELF':\n-        raise ParseError(f\"Invalid magic {ident.magic!r}\")\n+        raise binparsing.ParseError(f\"Invalid magic {ident.magic!r}\")\n \n     if ident.data != Endianness.little:\n-        raise ParseError(\"Big endian is unsupported\")\n+        raise binparsing.ParseError(\"Big endian is unsupported\")\n \n     if ident.version != 1:\n-        raise ParseError(f\"Only version 1 is supported, not {ident.version}\")\n+        raise binparsing.ParseError(f\"Only version 1 is supported, not {ident.version}\")\n \n     header = Header.parse(f, bitness=ident.klass)\n \n     # Read string table\n-    _safe_seek(f, header.shoff + header.shstrndx * header.shentsize)\n+    binparsing.safe_seek(f, header.shoff + header.shstrndx * header.shentsize)\n     shstr = SectionHeader.parse(f, bitness=ident.klass)\n \n-    _safe_seek(f, shstr.offset)\n-    string_table = _safe_read(f, shstr.size)\n+    binparsing.safe_seek(f, shstr.offset)\n+    string_table = binparsing.safe_read(f, shstr.size)\n \n     # Back to all sections\n     for i in range(header.shnum):\n-        _safe_seek(f, header.shoff + i * header.shentsize)\n+        binparsing.safe_seek(f, header.shoff + i * header.shentsize)\n         sh = SectionHeader.parse(f, bitness=ident.klass)\n         name = string_table[sh.name:].split(b'\\x00')[0]\n         if name == b'.rodata':\n             return sh\n \n-    raise ParseError(\"No .rodata section found\")\n+    raise binparsing.ParseError(\"No .rodata section found\")\n \n \n @dataclasses.dataclass\n@@ -268,20 +221,49 @@ def _find_versions(data: bytes) -&gt; Versions:\n     Note that 'data' can actually be a mmap.mmap, but typing doesn't handle that\n     correctly: https://github.com/python/typeshed/issues/1467\n     \"\"\"\n-    match = re.search(\n-        br'\\x00QtWebEngine/([0-9.]+) Chrome/([0-9.]+)\\x00',\n-        data,\n-    )\n+    pattern = br'\\x00QtWebEngine/([0-9.]+) Chrome/([0-9.]+)\\x00'\n+    match = re.search(pattern, data)\n+    if match is not None:\n+        try:\n+            return Versions(\n+                webengine=match.group(1).decode('ascii'),\n+                chromium=match.group(2).decode('ascii'),\n+            )\n+        except UnicodeDecodeError as e:\n+            raise binparsing.ParseError(e)\n+\n+    # Here it gets even more crazy: Sometimes, we don't have the full UA in one piece\n+    # in the string table somehow (?!). However, Qt 6.2 added a separate\n+    # qWebEngineChromiumVersion(), with PyQt wrappers following later. And *that*\n+    # apparently stores the full version in the string table separately from the UA.\n+    # As we clearly didn't have enough crazy heuristics yet so far, let's hunt for it!\n+\n+    # We first get the partial Chromium version from the UA:\n+    match = re.search(pattern[:-4], data)  # without trailing literal \\x00\n+    if match is None:\n+        raise binparsing.ParseError(\"No match in .rodata\")\n+\n+    webengine_bytes = match.group(1)\n+    partial_chromium_bytes = match.group(2)\n+    if b\".\" not in partial_chromium_bytes or len(partial_chromium_bytes) &lt; 6:\n+        # some sanity checking\n+        raise binparsing.ParseError(\"Inconclusive partial Chromium bytes\")\n+\n+    # And then try to find the *full* string, stored separately, based on the\n+    # partial one we got above.\n+    pattern = br\"\\x00(\" + re.escape(partial_chromium_bytes) + br\"[0-9.]+)\\x00\"\n+    match = re.search(pattern, data)\n     if match is None:\n-        raise ParseError(\"No match in .rodata\")\n+        raise binparsing.ParseError(\"No match in .rodata for full version\")\n \n+    chromium_bytes = match.group(1)\n     try:\n         return Versions(\n-            webengine=match.group(1).decode('ascii'),\n-            chromium=match.group(2).decode('ascii'),\n+            webengine=webengine_bytes.decode('ascii'),\n+            chromium=chromium_bytes.decode('ascii'),\n         )\n     except UnicodeDecodeError as e:\n-        raise ParseError(e)\n+        raise binparsing.ParseError(e)\n \n \n def _parse_from_file(f: IO[bytes]) -&gt; Versions:\n@@ -302,8 +284,8 @@ def _parse_from_file(f: IO[bytes]) -&gt; Versions:\n             return _find_versions(cast(bytes, mmap_data))\n     except (OSError, OverflowError) as e:\n         log.misc.debug(f\"mmap failed ({e}), falling back to reading\", exc_info=True)\n-        _safe_seek(f, sh.offset)\n-        data = _safe_read(f, sh.size)\n+        binparsing.safe_seek(f, sh.offset)\n+        data = binparsing.safe_read(f, sh.size)\n         return _find_versions(data)\n \n \n@@ -330,6 +312,6 @@ def parse_webenginecore() -&gt; Optional[Versions]:\n \n         log.misc.debug(f\"Got versions from ELF: {versions}\")\n         return versions\n-    except ParseError as e:\n+    except binparsing.ParseError as e:\n         log.misc.debug(f\"Failed to parse ELF: {e}\", exc_info=True)\n         return None\ndiff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py\nindex 8fcfb2803..d20b4ba0f 100644\n--- a/qutebrowser/misc/guiprocess.py\n+++ b/qutebrowser/misc/guiprocess.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"A QProcess which shows notifications in the GUI.\"\"\"\n \n@@ -23,12 +8,13 @@ import dataclasses\n import locale\n import shlex\n import shutil\n+import signal\n from typing import Mapping, Sequence, Dict, Optional\n \n from qutebrowser.qt.core import (pyqtSlot, pyqtSignal, QObject, QProcess,\n                           QProcessEnvironment, QByteArray, QUrl, Qt)\n \n-from qutebrowser.utils import message, log, utils, usertypes, version\n+from qutebrowser.utils import message, log, utils, usertypes, version, qtutils\n from qutebrowser.api import cmdutils, apitypes\n from qutebrowser.completion.models import miscmodels\n \n@@ -96,6 +82,29 @@ class ProcessOutcome:\n         assert self.code is not None\n         return self.status == QProcess.ExitStatus.NormalExit and self.code == 0\n \n+    def was_sigterm(self) -&gt; bool:\n+        \"\"\"Whether the process was terminated by a SIGTERM.\n+\n+        This must not be called if the process didn't exit yet.\n+        \"\"\"\n+        assert self.status is not None, \"Process didn't finish yet\"\n+        assert self.code is not None\n+        return (\n+            self.status == QProcess.ExitStatus.CrashExit and\n+            self.code == signal.SIGTERM\n+        )\n+\n+    def _crash_signal(self) -&gt; Optional[signal.Signals]:\n+        \"\"\"Get a Python signal (e.g. signal.SIGTERM) from a crashed process.\"\"\"\n+        assert self.status == QProcess.ExitStatus.CrashExit\n+        if self.code is None:\n+            return None\n+\n+        try:\n+            return signal.Signals(self.code)\n+        except ValueError:\n+            return None\n+\n     def __str__(self) -&gt; str:\n         if self.running:\n             return f\"{self.what.capitalize()} is running.\"\n@@ -106,7 +115,11 @@ class ProcessOutcome:\n         assert self.code is not None\n \n         if self.status == QProcess.ExitStatus.CrashExit:\n-            return f\"{self.what.capitalize()} crashed.\"\n+            msg = f\"{self.what.capitalize()} {self.state_str()} with status {self.code}\"\n+            sig = self._crash_signal()\n+            if sig is None:\n+                return f\"{msg}.\"\n+            return f\"{msg} ({sig.name}).\"\n         elif self.was_successful():\n             return f\"{self.what.capitalize()} exited successfully.\"\n \n@@ -124,6 +137,8 @@ class ProcessOutcome:\n             return 'running'\n         elif self.status is None:\n             return 'not started'\n+        elif self.was_sigterm():\n+            return 'terminated'\n         elif self.status == QProcess.ExitStatus.CrashExit:\n             return 'crashed'\n         elif self.was_successful():\n@@ -266,18 +281,21 @@ class GUIProcess(QObject):\n             QProcess.ProcessError.WriteError: f\"Write error for {what}\",\n             QProcess.ProcessError.ReadError: f\"Read error for {what}\",\n         }\n-        error_string = self._proc.errorString()\n-        msg = ': '.join([error_descriptions[error], error_string])\n \n         # We can't get some kind of error code from Qt...\n         # https://bugreports.qt.io/browse/QTBUG-44769\n         # but we pre-resolve the executable in Python, which also checks if it's\n         # runnable.\n-        if self.resolved_cmd is None:  # pragma: no branch\n-            msg += f'\\nHint: Make sure {self.cmd!r} exists and is executable'\n+        if self.resolved_cmd is None:\n+            # No point in showing the \"No program defined\" we got due to\n+            # passing None into Qt.\n+            error_string = f\"{self.cmd!r} doesn't exist or isn't executable\"\n             if version.is_flatpak():\n-                msg += ' inside the Flatpak container'\n+                error_string += \" inside the Flatpak container\"\n+        else:  # pragma: no cover\n+            error_string = self._proc.errorString()\n \n+        msg = ': '.join([error_descriptions[error], error_string])\n         message.error(msg)\n \n     def _elide_output(self, output: str) -&gt; str:\n@@ -316,16 +334,17 @@ class GUIProcess(QObject):\n                 message.error(\n                     self._elide_output(self.stderr), replace=f\"stderr-{self.pid}\")\n \n-        if self.outcome.was_successful():\n+        msg = f\"{self.outcome} See :process {self.pid} for details.\"\n+        if self.outcome.was_successful() or self.outcome.was_sigterm():\n             if self.verbose:\n-                message.info(str(self.outcome))\n+                message.info(msg)\n             self._cleanup_timer.start()\n         else:\n             if self.stdout:\n                 log.procs.error(\"Process stdout:\\n\" + self.stdout.strip())\n             if self.stderr:\n                 log.procs.error(\"Process stderr:\\n\" + self.stderr.strip())\n-            message.error(str(self.outcome) + \" See :process for details.\")\n+            message.error(msg)\n \n     @pyqtSlot()\n     def _on_started(self) -&gt; None:\n@@ -362,7 +381,7 @@ class GUIProcess(QObject):\n         log.procs.debug(\"Starting process.\")\n         self._pre_start(cmd, args)\n         self._proc.start(\n-            self.resolved_cmd,  # type: ignore[arg-type]\n+            qtutils.remove_optional(self.resolved_cmd),\n             args,\n         )\n         self._post_start()\ndiff --git a/qutebrowser/misc/httpclient.py b/qutebrowser/misc/httpclient.py\nindex da205ae50..19186ffb7 100644\n--- a/qutebrowser/misc/httpclient.py\n+++ b/qutebrowser/misc/httpclient.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"An HTTP client based on QNetworkAccessManager.\"\"\"\n \n@@ -27,7 +12,7 @@ from qutebrowser.qt.core import pyqtSignal, QObject, QTimer\n from qutebrowser.qt.network import (QNetworkAccessManager, QNetworkRequest,\n                              QNetworkReply)\n \n-from qutebrowser.utils import log\n+from qutebrowser.utils import qtlog, usertypes\n \n \n class HTTPRequest(QNetworkRequest):\n@@ -61,7 +46,7 @@ class HTTPClient(QObject):\n \n     def __init__(self, parent=None):\n         super().__init__(parent)\n-        with log.disable_qt_msghandler():\n+        with qtlog.disable_qt_msghandler():\n             # WORKAROUND for a hang when messages are printed, see our\n             # NetworkAccessManager subclass for details.\n             self._nam = QNetworkAccessManager(self)\n@@ -100,7 +85,7 @@ class HTTPClient(QObject):\n         if reply.isFinished():\n             self.on_reply_finished(reply)\n         else:\n-            timer = QTimer(self)\n+            timer = usertypes.Timer(self)\n             timer.setInterval(10000)\n             timer.timeout.connect(reply.abort)\n             timer.start()\ndiff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py\nindex d9bbefbbe..eefa2e3f3 100644\n--- a/qutebrowser/misc/ipc.py\n+++ b/qutebrowser/misc/ipc.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utilities for IPC with existing instances.\"\"\"\n \n@@ -25,12 +10,13 @@ import json\n import getpass\n import binascii\n import hashlib\n+from typing import Optional\n \n from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, Qt\n from qutebrowser.qt.network import QLocalSocket, QLocalServer, QAbstractSocket\n \n import qutebrowser\n-from qutebrowser.utils import log, usertypes, error, standarddir, utils\n+from qutebrowser.utils import log, usertypes, error, standarddir, utils, debug, qtutils\n from qutebrowser.qt import sip\n \n \n@@ -42,7 +28,7 @@ PROTOCOL_VERSION = 1\n \n \n # The ipc server instance\n-server = None\n+server: Optional[\"IPCServer\"] = None\n \n \n def _get_socketname_windows(basedir):\n@@ -107,12 +93,12 @@ class SocketError(Error):\n         \"\"\"\n         super().__init__()\n         self.action = action\n-        self.code = socket.error()\n-        self.message = socket.errorString()\n+        self.code: QLocalSocket.LocalSocketError = socket.error()\n+        self.message: str = socket.errorString()\n \n     def __str__(self):\n-        return \"Error while {}: {} (error {})\".format(\n-            self.action, self.message, self.code)\n+        return \"Error while {}: {} ({})\".format(\n+            self.action, self.message, debug.qenum_key(QLocalSocket, self.code))\n \n \n class ListenError(Error):\n@@ -131,12 +117,12 @@ class ListenError(Error):\n             local_server: The QLocalServer which has the error set.\n         \"\"\"\n         super().__init__()\n-        self.code = local_server.serverError()\n-        self.message = local_server.errorString()\n+        self.code: QAbstractSocket.SocketError = local_server.serverError()\n+        self.message: str = local_server.errorString()\n \n     def __str__(self):\n-        return \"Error while listening to IPC server: {} (error {})\".format(\n-            self.message, self.code)\n+        return \"Error while listening to IPC server: {} ({})\".format(\n+            self.message, debug.qenum_key(QAbstractSocket, self.code))\n \n \n class AddressInUseError(ListenError):\n@@ -190,7 +176,7 @@ class IPCServer(QObject):\n             self._atime_timer.timeout.connect(self.update_atime)\n             self._atime_timer.setTimerType(Qt.TimerType.VeryCoarseTimer)\n \n-        self._server = QLocalServer(self)\n+        self._server: Optional[QLocalServer] = QLocalServer(self)\n         self._server.newConnection.connect(self.handle_connection)\n \n         self._socket = None\n@@ -217,6 +203,7 @@ class IPCServer(QObject):\n \n     def listen(self):\n         \"\"\"Start listening on self._socketname.\"\"\"\n+        assert self._server is not None\n         log.ipc.debug(\"Listening as {}\".format(self._socketname))\n         if self._atime_timer is not None:  # pragma: no branch\n             self._atime_timer.start()\n@@ -254,17 +241,16 @@ class IPCServer(QObject):\n     @pyqtSlot()\n     def handle_connection(self):\n         \"\"\"Handle a new connection to the server.\"\"\"\n-        if self.ignored:\n+        if self.ignored or self._server is None:\n             return\n         if self._socket is not None:\n             log.ipc.debug(\"Got new connection but ignoring it because we're \"\n                           \"still handling another one (0x{:x}).\".format(\n                               id(self._socket)))\n             return\n-        socket = self._server.nextPendingConnection()\n+        socket = qtutils.add_optional(self._server.nextPendingConnection())\n         if socket is None:\n-            log.ipc.debug(  # type: ignore[unreachable]\n-                \"No new connection to handle.\")\n+            log.ipc.debug(\"No new connection to handle.\")\n             return\n         log.ipc.debug(\"Client connected (socket 0x{:x}).\".format(id(socket)))\n         self._socket = socket\n@@ -273,13 +259,18 @@ class IPCServer(QObject):\n         if socket.canReadLine():\n             log.ipc.debug(\"We can read a line immediately.\")\n             self.on_ready_read()\n-        socket.error.connect(self.on_error)\n-        if socket.error() not in [  # type: ignore[operator]\n+\n+        socket.errorOccurred.connect(self.on_error)\n+\n+        # FIXME:v4 Ignore needed due to overloaded signal/method in Qt 5\n+        socket_error = socket.error()  # type: ignore[operator,unused-ignore]\n+        if socket_error not in [\n             QLocalSocket.LocalSocketError.UnknownSocketError,\n             QLocalSocket.LocalSocketError.PeerClosedError\n         ]:\n             log.ipc.debug(\"We got an error immediately.\")\n-            self.on_error(socket.error())  # type: ignore[operator]\n+            self.on_error(socket_error)\n+\n         socket.disconnected.connect(self.on_disconnected)\n         if socket.state() == QLocalSocket.LocalSocketState.UnconnectedState:\n             log.ipc.debug(\"Socket was disconnected immediately.\")\n@@ -304,7 +295,7 @@ class IPCServer(QObject):\n         log.ipc.error(\"Ignoring invalid IPC data from socket 0x{:x}.\".format(\n             id(self._socket)))\n         self.got_invalid_data.emit()\n-        self._socket.error.connect(self.on_error)\n+        self._socket.errorOccurred.connect(self.on_error)\n         self._socket.disconnectFromServer()\n \n     def _handle_data(self, data):\n@@ -400,6 +391,11 @@ class IPCServer(QObject):\n     def on_timeout(self):\n         \"\"\"Cancel the current connection if it was idle for too long.\"\"\"\n         assert self._socket is not None\n+        if not self._timer.check_timeout_validity():\n+            # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-124496\n+            log.ipc.debug(\"Ignoring early on_timeout call\")\n+            return\n+\n         log.ipc.error(\"IPC connection timed out \"\n                       \"(socket 0x{:x}).\".format(id(self._socket)))\n         self._socket.disconnectFromServer()\n@@ -420,6 +416,7 @@ class IPCServer(QObject):\n         access time timestamp modified at least once every 6 hours of monotonic\n         time or the 'sticky' bit should be set on the file.\n         \"\"\"\n+        assert self._server is not None\n         path = self._server.fullServerName()\n         if not path:\n             log.ipc.error(\"In update_atime with no server path!\")\n@@ -438,11 +435,19 @@ class IPCServer(QObject):\n     @pyqtSlot()\n     def shutdown(self):\n         \"\"\"Shut down the IPC server cleanly.\"\"\"\n+        if self._server is None:\n+            # We can get called twice when using :restart -- there, IPC is shut down\n+            # early to avoid processing new connections while shutting down, and then\n+            # we get called again when the application is about to quit.\n+            return\n+\n         log.ipc.debug(\"Shutting down IPC (socket 0x{:x})\".format(\n             id(self._socket)))\n+\n         if self._socket is not None:\n             self._socket.deleteLater()\n             self._socket = None\n+\n         self._timer.stop()\n         if self._atime_timer is not None:  # pragma: no branch\n             self._atime_timer.stop()\n@@ -450,9 +455,11 @@ class IPCServer(QObject):\n                 self._atime_timer.timeout.disconnect(self.update_atime)\n             except TypeError:\n                 pass\n+\n         self._server.close()\n         self._server.deleteLater()\n         self._remove_server()\n+        self._server = None\n \n \n def send_to_running_instance(socketname, command, target_arg, *, socket=None):\n@@ -502,8 +509,8 @@ def send_to_running_instance(socketname, command, target_arg, *, socket=None):\n         if socket.error() not in [QLocalSocket.LocalSocketError.ConnectionRefusedError,\n                                   QLocalSocket.LocalSocketError.ServerNotFoundError]:\n             raise SocketError(\"connecting to running instance\", socket)\n-        log.ipc.debug(\"No existing instance present (error {})\".format(\n-            socket.error()))\n+        log.ipc.debug(\"No existing instance present ({})\".format(\n+            debug.qenum_key(QLocalSocket, socket.error())))\n         return False\n \n \ndiff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py\nindex cdfed8392..32867c17a 100644\n--- a/qutebrowser/misc/keyhintwidget.py\n+++ b/qutebrowser/misc/keyhintwidget.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Ryan Roden-Corrent (rcorre) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Small window that pops up to show hints for possible keystrings.\n \ndiff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py\nindex a270798b5..c96109e9e 100644\n--- a/qutebrowser/misc/lineparser.py\n+++ b/qutebrowser/misc/lineparser.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Parser for line-based files like histories.\"\"\"\n \ndiff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py\nindex 080f22d46..7ca409afe 100644\n--- a/qutebrowser/misc/miscwidgets.py\n+++ b/qutebrowser/misc/miscwidgets.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Misc. widgets used at different places.\"\"\"\n \n@@ -23,51 +8,16 @@ from typing import Optional\n \n from qutebrowser.qt.core import pyqtSlot, pyqtSignal, Qt, QSize, QTimer\n from qutebrowser.qt.widgets import (QLineEdit, QWidget, QHBoxLayout, QLabel,\n-                             QStyleOption, QStyle, QLayout, QApplication,\n-                             QSplitter)\n+                             QStyleOption, QStyle, QLayout, QSplitter)\n from qutebrowser.qt.gui import QValidator, QPainter, QResizeEvent\n \n from qutebrowser.config import config, configfiles\n-from qutebrowser.utils import utils, log, usertypes, debug\n+from qutebrowser.utils import utils, log, usertypes, debug, qtutils\n from qutebrowser.misc import cmdhistory\n from qutebrowser.browser import inspector\n from qutebrowser.keyinput import keyutils, modeman\n \n \n-class MinimalLineEditMixin:\n-\n-    \"\"\"A mixin to give a QLineEdit a minimal look and nicer repr().\"\"\"\n-\n-    def __init__(self):\n-        self.setStyleSheet(  # type: ignore[attr-defined]\n-            \"\"\"\n-            QLineEdit {\n-                border: 0px;\n-                padding-left: 1px;\n-                background-color: transparent;\n-            }\n-            \"\"\"\n-        )\n-        self.setAttribute(  # type: ignore[attr-defined]\n-            Qt.WidgetAttribute.WA_MacShowFocusRect, False)\n-\n-    def keyPressEvent(self, e):\n-        \"\"\"Override keyPressEvent to paste primary selection on Shift + Ins.\"\"\"\n-        if e.key() == Qt.Key.Key_Insert and e.modifiers() == Qt.KeyboardModifier.ShiftModifier:\n-            try:\n-                text = utils.get_clipboard(selection=True, fallback=True)\n-            except utils.ClipboardError:\n-                e.ignore()\n-            else:\n-                e.accept()\n-                self.insert(text)  # type: ignore[attr-defined]\n-            return\n-        super().keyPressEvent(e)  # type: ignore[misc]\n-\n-    def __repr__(self):\n-        return utils.get_repr(self)\n-\n-\n class CommandLineEdit(QLineEdit):\n \n     \"\"\"A QLineEdit with a history and prompt chars.\n@@ -78,7 +28,7 @@ class CommandLineEdit(QLineEdit):\n         _promptlen: The length of the current prompt.\n     \"\"\"\n \n-    def __init__(self, *, parent=None):\n+    def __init__(self, parent=None):\n         super().__init__(parent)\n         self.history = cmdhistory.History(parent=self)\n         self._validator = _CommandValidator(self)\n@@ -222,7 +172,10 @@ class _FoldArrow(QWidget):\n             elem = QStyle.PrimitiveElement.PE_IndicatorArrowRight\n         else:\n             elem = QStyle.PrimitiveElement.PE_IndicatorArrowDown\n-        self.style().drawPrimitive(elem, opt, painter, self)\n+\n+        style = self.style()\n+        assert style is not None\n+        style.drawPrimitive(elem, opt, painter, self)\n \n     def minimumSizeHint(self):\n         \"\"\"Return a sensible size.\"\"\"\n@@ -278,10 +231,10 @@ class WrapperLayout(QLayout):\n         if self._widget is None:\n             return\n         assert self._container is not None\n-        self._widget.setParent(None)  # type: ignore[call-overload]\n+        self._widget.setParent(qtutils.QT_NONE)\n         self._widget.deleteLater()\n         self._widget = None\n-        self._container.setFocusProxy(None)  # type: ignore[arg-type]\n+        self._container.setFocusProxy(qtutils.QT_NONE)\n \n \n class FullscreenNotification(QLabel):\n@@ -307,9 +260,17 @@ class FullscreenNotification(QLabel):\n \n         self.resize(self.sizeHint())\n         if config.val.content.fullscreen.window:\n-            geom = self.parentWidget().geometry()\n+            parent = self.parentWidget()\n+            assert parent is not None\n+            geom = parent.geometry()\n         else:\n-            geom = QApplication.desktop().screenGeometry(self)\n+            window = self.window()\n+            assert window is not None\n+            handle = window.windowHandle()\n+            assert handle is not None\n+            screen = handle.screen()\n+            assert screen is not None\n+            geom = screen.geometry()\n         self.move((geom.width() - self.sizeHint().width()) // 2, 30)\n \n     def set_timeout(self, timeout):\n@@ -364,6 +325,8 @@ class InspectorSplitter(QSplitter):\n \n         main_widget = self.widget(self._main_idx)\n         inspector_widget = self.widget(self._inspector_idx)\n+        assert main_widget is not None\n+        assert inspector_widget is not None\n \n         if not inspector_widget.isVisible():\n             raise inspector.Error(\"No inspector inside main window\")\n@@ -476,8 +439,9 @@ class InspectorSplitter(QSplitter):\n         self._preferred_size = sizes[self._inspector_idx]\n         self._save_preferred_size()\n \n-    def resizeEvent(self, e: QResizeEvent) -&gt; None:\n+    def resizeEvent(self, e: Optional[QResizeEvent]) -&gt; None:\n         \"\"\"Window resize event.\"\"\"\n+        assert e is not None\n         super().resizeEvent(e)\n         if self.count() == 2:\n             self._adjust_size()\n@@ -499,8 +463,8 @@ class KeyTesterWidget(QWidget):\n         lines = [\n             str(keyutils.KeyInfo.from_event(e)),\n             '',\n-            f\"key: {debug.qenum_key(Qt.Key, e.key(), klass=Qt.Key)}\",\n-            f\"modifiers: {debug.qflags_key(Qt.KeyboardModifier, e.modifiers())}\",\n+            f\"key: {debug.qenum_key(Qt, e.key(), klass=Qt.Key)}\",\n+            f\"modifiers: {debug.qflags_key(Qt, e.modifiers())}\",\n             'text: {!r}'.format(e.text()),\n         ]\n         self._label.setText('\\n'.join(lines))\ndiff --git a/qutebrowser/misc/msgbox.py b/qutebrowser/misc/msgbox.py\nindex 45ad2d44a..af04b9345 100644\n--- a/qutebrowser/misc/msgbox.py\n+++ b/qutebrowser/misc/msgbox.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Convenience functions to show message boxes.\"\"\"\n \ndiff --git a/qutebrowser/misc/nativeeventfilter.py b/qutebrowser/misc/nativeeventfilter.py\nnew file mode 100644\nindex 000000000..06533bd42\n--- /dev/null\n+++ b/qutebrowser/misc/nativeeventfilter.py\n@@ -0,0 +1,187 @@\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n+\"\"\"Native Qt event filter.\n+\n+This entire file is a giant WORKAROUND for https://bugreports.qt.io/browse/QTBUG-114334.\n+\"\"\"\n+\n+from typing import Tuple, Union, cast, Optional\n+import enum\n+import ctypes\n+import ctypes.util\n+\n+from qutebrowser.qt import sip, machinery\n+from qutebrowser.qt.core import QAbstractNativeEventFilter, QByteArray, qVersion\n+\n+from qutebrowser.misc import objects\n+from qutebrowser.utils import log\n+\n+\n+# Needs to be saved to avoid garbage collection\n+_instance: Optional[\"NativeEventFilter\"] = None\n+\n+# Using C-style naming for C structures in this file\n+# pylint: disable=invalid-name\n+\n+\n+class xcb_ge_generic_event_t(ctypes.Structure):  # noqa: N801\n+    \"\"\"See https://xcb.freedesktop.org/manual/structxcb__ge__generic__event__t.html.\n+\n+    Also used for xcb_generic_event_t as the structures overlap:\n+    https://xcb.freedesktop.org/manual/structxcb__generic__event__t.html\n+    \"\"\"\n+\n+    _fields_ = [\n+        (\"response_type\", ctypes.c_uint8),\n+        (\"extension\", ctypes.c_uint8),\n+        (\"sequence\", ctypes.c_uint16),\n+        (\"length\", ctypes.c_uint32),\n+        (\"event_type\", ctypes.c_uint16),\n+        (\"pad0\", ctypes.c_uint8 * 22),\n+        (\"full_sequence\", ctypes.c_uint32),\n+    ]\n+\n+\n+_XCB_GE_GENERIC = 35\n+\n+\n+class XcbInputOpcodes(enum.IntEnum):\n+\n+    \"\"\"https://xcb.freedesktop.org/manual/group__XCB__Input__API.html.\n+\n+    NOTE: If adding anything new here, adjust _PROBLEMATIC_XINPUT_EVENTS below!\n+    \"\"\"\n+\n+    HIERARCHY = 11\n+\n+    TOUCH_BEGIN = 18\n+    TOUCH_UPDATE = 19\n+    TOUCH_END = 20\n+\n+    GESTURE_PINCH_BEGIN = 27\n+    GESTURE_PINCH_UPDATE = 28\n+    GESTURE_PINCH_END = 29\n+\n+    GESTURE_SWIPE_BEGIN = 30\n+    GESTURE_SWIPE_UPDATE = 31\n+    GESTURE_SWIPE_END = 32\n+\n+\n+_PROBLEMATIC_XINPUT_EVENTS = set(XcbInputOpcodes) - {XcbInputOpcodes.HIERARCHY}\n+\n+\n+class xcb_query_extension_reply_t(ctypes.Structure):  # noqa: N801\n+    \"\"\"https://xcb.freedesktop.org/manual/structxcb__query__extension__reply__t.html.\"\"\"\n+\n+    _fields_ = [\n+        (\"response_type\", ctypes.c_uint8),\n+        (\"pad0\", ctypes.c_uint8),\n+        (\"sequence\", ctypes.c_uint16),\n+        (\"length\", ctypes.c_uint32),\n+        (\"present\", ctypes.c_uint8),\n+        (\"major_opcode\", ctypes.c_uint8),\n+        (\"first_event\", ctypes.c_uint8),\n+        (\"first_error\", ctypes.c_uint8),\n+    ]\n+\n+\n+# pylint: enable=invalid-name\n+\n+\n+if machinery.IS_QT6:\n+    _PointerRetType = sip.voidptr\n+else:\n+    _PointerRetType = int\n+\n+\n+class NativeEventFilter(QAbstractNativeEventFilter):\n+\n+    \"\"\"Event filter for XCB messages to work around Qt 6.5.1 crash.\"\"\"\n+\n+    # Return values for nativeEventFilter.\n+    #\n+    # Tuple because PyQt uses the second value as the *result out-pointer, which\n+    # according to the Qt documentation is only used on Windows.\n+    _PASS_EVENT_RET = (False, cast(_PointerRetType, 0))\n+    _FILTER_EVENT_RET = (True, cast(_PointerRetType, 0))\n+\n+    def __init__(self) -&gt; None:\n+        super().__init__()\n+        self._active = False  # Set to true when getting hierarchy event\n+\n+        xcb = ctypes.CDLL(ctypes.util.find_library(\"xcb\"))\n+        xcb.xcb_connect.restype = ctypes.POINTER(ctypes.c_void_p)\n+        xcb.xcb_query_extension_reply.restype = ctypes.POINTER(\n+            xcb_query_extension_reply_t\n+        )\n+\n+        conn = xcb.xcb_connect(None, None)\n+        assert conn\n+\n+        try:\n+            assert not xcb.xcb_connection_has_error(conn)\n+\n+            # Get major opcode ID of Xinput extension\n+            name = b\"XInputExtension\"\n+            cookie = xcb.xcb_query_extension(conn, len(name), name)\n+            reply = xcb.xcb_query_extension_reply(conn, cookie, None)\n+            assert reply\n+\n+            if reply.contents.present:\n+                self.xinput_opcode = reply.contents.major_opcode\n+            else:\n+                self.xinput_opcode = None\n+        finally:\n+            xcb.xcb_disconnect(conn)\n+\n+    def nativeEventFilter(\n+        self, evtype: Union[bytes, QByteArray], message: Optional[sip.voidptr]\n+    ) -&gt; Tuple[bool, _PointerRetType]:\n+        \"\"\"Handle XCB events.\"\"\"\n+        # We're only installed when the platform plugin is xcb\n+        assert evtype == b\"xcb_generic_event_t\", evtype\n+        assert message is not None\n+\n+        # We cast to xcb_ge_generic_event_t, which overlaps with xcb_generic_event_t.\n+        # .extension and .event_type will only make sense if this is an\n+        # XCB_GE_GENERIC event, but this is the first thing we check in the 'if'\n+        # below anyways.\n+        event = ctypes.cast(\n+            int(message), ctypes.POINTER(xcb_ge_generic_event_t)\n+        ).contents\n+\n+        if (\n+            event.response_type == _XCB_GE_GENERIC\n+            and event.extension == self.xinput_opcode\n+        ):\n+            if not self._active and event.event_type == XcbInputOpcodes.HIERARCHY:\n+                log.misc.warning(\n+                    \"Got XInput HIERARCHY event, future swipe/pinch/touch events will \"\n+                    \"be ignored to avoid a Qt 6.5.1 crash. Restart qutebrowser to make \"\n+                    \"them work again.\"\n+                )\n+                self._active = True\n+            elif self._active and event.event_type in _PROBLEMATIC_XINPUT_EVENTS:\n+                name = XcbInputOpcodes(event.event_type).name\n+                log.misc.debug(f\"Ignoring problematic XInput event {name}\")\n+                return self._FILTER_EVENT_RET\n+\n+        return self._PASS_EVENT_RET\n+\n+\n+def init() -&gt; None:\n+    \"\"\"Install the native event filter if needed.\"\"\"\n+    global _instance\n+\n+    platform = objects.qapp.platformName()\n+    qt_version = qVersion()\n+    log.misc.debug(f\"Platform {platform}, Qt {qt_version}\")\n+\n+    if platform != \"xcb\" or qt_version != \"6.5.1\":\n+        return\n+\n+    log.misc.debug(\"Installing native event filter to work around Qt 6.5.1 crash\")\n+    _instance = NativeEventFilter()\n+    objects.qapp.installNativeEventFilter(_instance)\ndiff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py\nindex 63a9cb2dd..1b91c6fdd 100644\n--- a/qutebrowser/misc/objects.py\n+++ b/qutebrowser/misc/objects.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Various global objects.\"\"\"\n \ndiff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py\nnew file mode 100644\nindex 000000000..c0e6b4d0c\n--- /dev/null\n+++ b/qutebrowser/misc/pakjoy.py\n@@ -0,0 +1,276 @@\n+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n+\"\"\"Chromium .pak repacking.\n+\n+This entire file is a great WORKAROUND for https://bugreports.qt.io/browse/QTBUG-118157\n+and the fact we can't just simply disable the hangouts extension:\n+https://bugreports.qt.io/browse/QTBUG-118452\n+\n+It's yet another big hack. If you think this is bad, look at elf.py instead.\n+\n+The name of this file might or might not be inspired by a certain vegetable,\n+as well as the \"joy\" this bug has caused me.\n+\n+Useful references:\n+\n+- https://sweetscape.com/010editor/repository/files/PAK.bt (010 editor &lt;3)\n+- https://textslashplain.com/2022/05/03/chromium-internals-pak-files/\n+- https://github.com/myfreeer/chrome-pak-customizer\n+- https://source.chromium.org/chromium/chromium/src/+/main:tools/grit/pak_util.py\n+- https://source.chromium.org/chromium/chromium/src/+/main:tools/grit/grit/format/data_pack.py\n+\n+This is a \"best effort\" parser. If it errors out, we don't apply the workaround\n+instead of crashing.\n+\"\"\"\n+\n+import os\n+import shutil\n+import pathlib\n+import dataclasses\n+import contextlib\n+from typing import ClassVar, IO, Optional, Dict, Tuple, Iterator\n+\n+from qutebrowser.config import config\n+from qutebrowser.misc import binparsing, objects\n+from qutebrowser.utils import qtutils, standarddir, version, utils, log\n+\n+HANGOUTS_MARKER = b\"// Extension ID: nkeimhogjdpnpccoofpliimaahmaaome\"\n+HANGOUTS_IDS = [\n+    36197,  # QtWebEngine 6.5, as found by toofar\n+    34897,  # QtWebEngine 6.4\n+]\n+PAK_VERSION = 5\n+RESOURCES_ENV_VAR = \"QTWEBENGINE_RESOURCES_PATH\"\n+DISABLE_ENV_VAR = \"QUTE_DISABLE_PAKJOY\"\n+CACHE_DIR_NAME = \"webengine_resources_pak_quirk\"\n+PAK_FILENAME = \"qtwebengine_resources.pak\"\n+\n+TARGET_URL = b\"https://*.google.com/*\"\n+REPLACEMENT_URL = b\"https://qute.invalid/*\"\n+assert len(TARGET_URL) == len(REPLACEMENT_URL)\n+\n+\n+@dataclasses.dataclass\n+class PakHeader:\n+\n+    \"\"\"Chromium .pak header (version 5).\"\"\"\n+\n+    encoding: int  # uint32\n+    resource_count: int  # uint16\n+    _alias_count: int  # uint16\n+\n+    _FORMAT: ClassVar[str] = \" \"PakHeader\":\n+        \"\"\"Parse a PAK version 5 header from a file.\"\"\"\n+        return cls(*binparsing.unpack(cls._FORMAT, fobj))\n+\n+\n+@dataclasses.dataclass\n+class PakEntry:\n+\n+    \"\"\"Entry description in a .pak file.\"\"\"\n+\n+    resource_id: int  # uint16\n+    file_offset: int  # uint32\n+    size: int = 0  # not in file\n+\n+    _FORMAT: ClassVar[str] = \" \"PakEntry\":\n+        \"\"\"Parse a PAK entry from a file.\"\"\"\n+        return cls(*binparsing.unpack(cls._FORMAT, fobj))\n+\n+\n+class PakParser:\n+    \"\"\"Parse webengine pak and find patch location to disable Google Meet extension.\"\"\"\n+\n+    def __init__(self, fobj: IO[bytes]) -&gt; None:\n+        \"\"\"Parse the .pak file from the given file object.\"\"\"\n+        pak_version = binparsing.unpack(\" int:\n+        \"\"\"Return byte offset of TARGET_URL into the pak file.\"\"\"\n+        try:\n+            return self.manifest_entry.file_offset + self.manifest.index(TARGET_URL)\n+        except ValueError:\n+            raise binparsing.ParseError(\"Couldn't find URL in manifest\")\n+\n+    def _maybe_get_hangouts_manifest(self, entry: PakEntry) -&gt; Optional[bytes]:\n+        self.fobj.seek(entry.file_offset)\n+        data = self.fobj.read(entry.size)\n+\n+        if not data.startswith(b\"{\") or not data.rstrip(b\"\\n\").endswith(b\"}\"):\n+            # not JSON\n+            return None\n+\n+        if HANGOUTS_MARKER not in data:\n+            return None\n+\n+        return data\n+\n+    def _read_header(self) -&gt; Dict[int, PakEntry]:\n+        \"\"\"Read the header and entry index from the .pak file.\"\"\"\n+        entries = []\n+\n+        header = PakHeader.parse(self.fobj)\n+        for _ in range(header.resource_count + 1):  # + 1 due to sentinel at end\n+            entries.append(PakEntry.parse(self.fobj))\n+\n+        for entry, next_entry in zip(entries, entries[1:]):\n+            if entry.resource_id == 0:\n+                raise binparsing.ParseError(\"Unexpected sentinel entry\")\n+            entry.size = next_entry.file_offset - entry.file_offset\n+\n+        if entries[-1].resource_id != 0:\n+            raise binparsing.ParseError(\"Missing sentinel entry\")\n+        del entries[-1]\n+\n+        return {entry.resource_id: entry for entry in entries}\n+\n+    def _find_manifest(self, entries: Dict[int, PakEntry]) -&gt; Tuple[PakEntry, bytes]:\n+        to_check = list(entries.values())\n+        for hangouts_id in HANGOUTS_IDS:\n+            if hangouts_id in entries:\n+                # Most likely candidate, based on previous known ID\n+                to_check.insert(0, entries[hangouts_id])\n+\n+        for entry in to_check:\n+            manifest = self._maybe_get_hangouts_manifest(entry)\n+            if manifest is not None:\n+                return entry, manifest\n+\n+        raise binparsing.ParseError(\"Couldn't find hangouts manifest\")\n+\n+\n+def _find_webengine_resources() -&gt; pathlib.Path:\n+    \"\"\"Find the QtWebEngine resources dir.\n+\n+    Mirrors logic from QtWebEngine:\n+    https://github.com/qt/qtwebengine/blob/v6.6.0/src/core/web_engine_library_info.cpp#L293-L341\n+    \"\"\"\n+    if RESOURCES_ENV_VAR in os.environ:\n+        return pathlib.Path(os.environ[RESOURCES_ENV_VAR])\n+\n+    candidates = []\n+    qt_data_path = qtutils.library_path(qtutils.LibraryPath.data)\n+    if utils.is_mac:  # pragma: no cover\n+        # I'm not sure how to arrive at this path without hardcoding it\n+        # ourselves. importlib_resources(\"PyQt6.Qt6\") can serve as a\n+        # replacement for the qtutils bit but it doesn't seem to help find the\n+        # actual Resources folder.\n+        candidates.append(\n+            qt_data_path / \"lib\" / \"QtWebEngineCore.framework\" / \"Resources\"\n+        )\n+\n+    candidates += [\n+        qt_data_path / \"resources\",\n+        qt_data_path,\n+        pathlib.Path(objects.qapp.applicationDirPath()),\n+        pathlib.Path.home() / f\".{objects.qapp.applicationName()}\",\n+    ]\n+\n+    for candidate in candidates:\n+        if (candidate / PAK_FILENAME).exists():\n+            return candidate\n+\n+    candidates_str = \"\\n\".join(f\"    {p}\" for p in candidates)\n+    raise FileNotFoundError(\n+        f\"Couldn't find webengine resources dir, candidates:\\n{candidates_str}\")\n+\n+\n+def copy_webengine_resources() -&gt; Optional[pathlib.Path]:\n+    \"\"\"Copy qtwebengine resources to local dir for patching.\"\"\"\n+    resources_dir = _find_webengine_resources()\n+    work_dir = pathlib.Path(standarddir.cache()) / CACHE_DIR_NAME\n+\n+    if work_dir.exists():\n+        log.misc.debug(f\"Removing existing {work_dir}\")\n+        shutil.rmtree(work_dir)\n+\n+    versions = version.qtwebengine_versions(avoid_init=True)\n+    if not (\n+        # https://bugreports.qt.io/browse/QTBUG-118157\n+        versions.webengine == utils.VersionNumber(6, 6)\n+        # https://bugreports.qt.io/browse/QTBUG-113369\n+        or (\n+            versions.webengine &gt;= utils.VersionNumber(6, 5)\n+            and versions.webengine &lt; utils.VersionNumber(6, 5, 3)\n+            and config.val.colors.webpage.darkmode.enabled\n+        )\n+    ):\n+        # No patching needed\n+        return None\n+\n+    log.misc.debug(\n+        \"Copying webengine resources for quirk patching: \"\n+        f\"{resources_dir} -&gt; {work_dir}\"\n+    )\n+\n+    shutil.copytree(resources_dir, work_dir)\n+    return work_dir\n+\n+\n+def _patch(file_to_patch: pathlib.Path) -&gt; None:\n+    \"\"\"Apply any patches to the given pak file.\"\"\"\n+    if not file_to_patch.exists():\n+        log.misc.error(\n+            \"Resource pak doesn't exist at expected location! \"\n+            f\"Not applying quirks. Expected location: {file_to_patch}\"\n+        )\n+        return\n+\n+    with open(file_to_patch, \"r+b\") as f:\n+        try:\n+            parser = PakParser(f)\n+            log.misc.debug(f\"Patching pak entry: {parser.manifest_entry}\")\n+            offset = parser.find_patch_offset()\n+            binparsing.safe_seek(f, offset)\n+            f.write(REPLACEMENT_URL)\n+        except binparsing.ParseError:\n+            log.misc.exception(\"Failed to apply quirk to resources pak.\")\n+\n+\n+@contextlib.contextmanager\n+def patch_webengine() -&gt; Iterator[None]:\n+    \"\"\"Apply any patches to webengine resource pak files.\"\"\"\n+    if os.environ.get(DISABLE_ENV_VAR):\n+        log.misc.debug(f\"Not applying quirk due to {DISABLE_ENV_VAR}\")\n+        yield\n+        return\n+\n+    try:\n+        # Still calling this on Qt != 6.6 so that the directory is cleaned up\n+        # when not needed anymore.\n+        webengine_resources_path = copy_webengine_resources()\n+    except OSError:\n+        log.misc.exception(\"Failed to copy webengine resources, not applying quirk\")\n+        yield\n+        return\n+\n+    if webengine_resources_path is None:\n+        yield\n+        return\n+\n+    _patch(webengine_resources_path / PAK_FILENAME)\n+\n+    old_value = os.environ.get(RESOURCES_ENV_VAR)\n+    os.environ[RESOURCES_ENV_VAR] = str(webengine_resources_path)\n+\n+    yield\n+\n+    # Restore old value for subprocesses or :restart\n+    if old_value is None:\n+        del os.environ[RESOURCES_ENV_VAR]\n+    else:\n+        os.environ[RESOURCES_ENV_VAR] = old_value\ndiff --git a/qutebrowser/misc/pastebin.py b/qutebrowser/misc/pastebin.py\nindex afca59d2e..cc1096527 100644\n--- a/qutebrowser/misc/pastebin.py\n+++ b/qutebrowser/misc/pastebin.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Client for the pastebin.\"\"\"\n \ndiff --git a/qutebrowser/misc/quitter.py b/qutebrowser/misc/quitter.py\nindex 3169e8bfa..825acfcd8 100644\n--- a/qutebrowser/misc/quitter.py\n+++ b/qutebrowser/misc/quitter.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Helpers related to quitting qutebrowser cleanly.\"\"\"\n \n@@ -28,6 +13,7 @@ import shutil\n import argparse\n import tokenize\n import functools\n+import warnings\n import subprocess\n from typing import Iterable, Mapping, MutableSequence, Sequence, cast\n \n@@ -39,7 +25,7 @@ except ImportError:\n \n import qutebrowser\n from qutebrowser.api import cmdutils\n-from qutebrowser.utils import log\n+from qutebrowser.utils import log, qtlog\n from qutebrowser.misc import sessions, ipc, objects\n from qutebrowser.mainwindow import prompt\n from qutebrowser.completion.models import miscmodels\n@@ -194,11 +180,18 @@ class Quitter(QObject):\n         # Open a new process and immediately shutdown the existing one\n         try:\n             args = self._get_restart_args(pages, session, override_args)\n-            subprocess.Popen(args)  # pylint: disable=consider-using-with\n+            proc = subprocess.Popen(args)  # pylint: disable=consider-using-with\n         except OSError:\n             log.destroy.exception(\"Failed to restart\")\n             return False\n         else:\n+            log.destroy.debug(f\"New process PID: {proc.pid}\")\n+            # Avoid ResourceWarning about still running subprocess when quitting.\n+            warnings.filterwarnings(\n+                \"ignore\",\n+                category=ResourceWarning,\n+                message=f\"subprocess {proc.pid} is still running\",\n+            )\n             return True\n \n     def shutdown(self, status: int = 0,\n@@ -221,21 +214,19 @@ class Quitter(QObject):\n             status, session))\n \n         sessions.shutdown(session, last_window=last_window)\n-\n-        if prompt.prompt_queue.shutdown():\n-            # If shutdown was called while we were asking a question, we're in\n-            # a still sub-eventloop (which gets quit now) and not in the main\n-            # one.\n-            # This means we need to defer the real shutdown to when we're back\n-            # in the real main event loop, or we'll get a segfault.\n-            log.destroy.debug(\"Deferring real shutdown because question was \"\n-                              \"active.\")\n-            QTimer.singleShot(0, functools.partial(self._shutdown_2, status,\n-                                                   is_restart=is_restart))\n-        else:\n-            # If we have no questions to shut down, we are already in the real\n-            # event loop, so we can shut down immediately.\n-            self._shutdown_2(status, is_restart=is_restart)\n+        if prompt.prompt_queue is not None:\n+            prompt.prompt_queue.shutdown()\n+\n+        # If shutdown was called while we were asking a question, we're in\n+        # a still sub-eventloop (which gets quit now) and not in the main\n+        # one.\n+        # But there's also other situations where it's problematic to shut down\n+        # immediately (e.g. when we're just starting up).\n+        # This means we need to defer the real shutdown to when we're back\n+        # in the real main event loop, or we'll get a segfault.\n+        log.destroy.debug(\"Deferring shutdown stage 2\")\n+        QTimer.singleShot(\n+            0, functools.partial(self._shutdown_2, status, is_restart=is_restart))\n \n     def _shutdown_2(self, status: int, is_restart: bool) -&gt; None:\n         \"\"\"Second stage of shutdown.\"\"\"\n@@ -282,12 +273,10 @@ def quit_(save: bool = False,\n     \"\"\"\n     if session is not None and not save:\n         raise cmdutils.CommandError(\"Session name given without --save!\")\n-    if save:\n-        if session is None:\n-            session = sessions.default\n-        instance.shutdown(session=session)\n-    else:\n-        instance.shutdown()\n+    if save and session is None:\n+        session = sessions.default\n+\n+    instance.shutdown(session=session)\n \n \n @cmdutils.register()\n@@ -311,5 +300,5 @@ def init(args: argparse.Namespace) -&gt; None:\n     \"\"\"Initialize the global Quitter instance.\"\"\"\n     global instance\n     instance = Quitter(args=args, parent=objects.qapp)\n-    instance.shutting_down.connect(log.shutdown_log)\n+    instance.shutting_down.connect(qtlog.shutdown_log)\n     objects.qapp.lastWindowClosed.connect(instance.on_last_window_closed)\ndiff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py\nindex ed1b90890..6017b3d2a 100644\n--- a/qutebrowser/misc/savemanager.py\n+++ b/qutebrowser/misc/savemanager.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Saving things to disk periodically.\"\"\"\n \ndiff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py\nindex e33bb23c5..dd63904cd 100644\n--- a/qutebrowser/misc/sessions.py\n+++ b/qutebrowser/misc/sessions.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Management of sessions - saved tabs/windows.\"\"\"\n \n@@ -31,7 +16,7 @@ from qutebrowser.qt.core import Qt, QUrl, QObject, QPoint, QTimer, QDateTime\n import yaml\n \n from qutebrowser.utils import (standarddir, objreg, qtutils, log, message,\n-                               utils, usertypes, version)\n+                               utils, usertypes)\n from qutebrowser.api import cmdutils\n from qutebrowser.config import config, configfiles\n from qutebrowser.completion.models import miscmodels\n@@ -64,12 +49,7 @@ def init(parent=None):\n \n     # WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/5359\n     backup_path = base_path / 'before-qt-515'\n-\n-    if objects.backend == usertypes.Backend.QtWebEngine:\n-        webengine_version = version.qtwebengine_versions().webengine\n-        do_backup = webengine_version &gt;= utils.VersionNumber(5, 15)\n-    else:\n-        do_backup = False\n+    do_backup = objects.backend == usertypes.Backend.QtWebEngine\n \n     if base_path.exists() and not backup_path.exists() and do_backup:\n         backup_path.mkdir()\n@@ -259,6 +239,13 @@ class SessionManager(QObject):\n         for idx, item in enumerate(history):\n             qtutils.ensure_valid(item)\n             item_data = self._save_tab_item(tab, idx, item)\n+\n+            if not item.url().isValid():\n+                # WORKAROUND Qt 6.5 regression\n+                # https://github.com/qutebrowser/qutebrowser/issues/7696\n+                log.sessions.debug(f\"Skipping invalid history item: {item}\")\n+                continue\n+\n             if item.url().scheme() == 'qute' and item.url().host() == 'back':\n                 # don't add qute://back to the session file\n                 if item_data.get('active', False) and data['history']:\n@@ -472,7 +459,6 @@ class SessionManager(QObject):\n         \"\"\"Turn yaml data into windows.\"\"\"\n         window = mainwindow.MainWindow(geometry=win['geometry'],\n                                        private=win.get('private', None))\n-        window.show()\n         tabbed_browser = objreg.get('tabbed-browser', scope='window',\n                                     window=window.win_id)\n         tab_to_focus = None\n@@ -485,6 +471,8 @@ class SessionManager(QObject):\n                 new_tab.set_pinned(True)\n         if tab_to_focus is not None:\n             tabbed_browser.widget.setCurrentIndex(tab_to_focus)\n+\n+        window.show()\n         if win.get('active', False):\n             QTimer.singleShot(0, tabbed_browser.widget.activateWindow)\n \n@@ -564,19 +552,17 @@ def session_load(name: str, *,\n     except SessionError as e:\n         raise cmdutils.CommandError(\"Error while loading session: {}\"\n                                     .format(e))\n-    else:\n-        if clear:\n-            for win in old_windows:\n-                win.close()\n-        if delete:\n-            try:\n-                session_manager.delete(name)\n-            except SessionError as e:\n-                log.sessions.exception(\"Error while deleting session!\")\n-                raise cmdutils.CommandError(\"Error while deleting session: {}\"\n-                                            .format(e))\n-            else:\n-                log.sessions.debug(\"Loaded &amp; deleted session {}.\".format(name))\n+    if clear:\n+        for win in old_windows:\n+            win.close()\n+    if delete:\n+        try:\n+            session_manager.delete(name)\n+        except SessionError as e:\n+            log.sessions.exception(\"Error while deleting session!\")\n+            raise cmdutils.CommandError(\"Error while deleting session: {}\"\n+                                        .format(e))\n+        log.sessions.debug(\"Loaded &amp; deleted session {}.\".format(name))\n \n \n @cmdutils.register()\n@@ -622,11 +608,10 @@ def session_save(name: ArgType = default, *,\n                                         with_history=not no_history)\n     except SessionError as e:\n         raise cmdutils.CommandError(\"Error while saving session: {}\".format(e))\n+    if quiet:\n+        log.sessions.debug(\"Saved session {}.\".format(name))\n     else:\n-        if quiet:\n-            log.sessions.debug(\"Saved session {}.\".format(name))\n-        else:\n-            message.info(\"Saved session {}.\".format(name))\n+        message.info(\"Saved session {}.\".format(name))\n \n \n @cmdutils.register()\n@@ -649,8 +634,7 @@ def session_delete(name: str, *, force: bool = False) -&gt; None:\n         log.sessions.exception(\"Error while deleting session!\")\n         raise cmdutils.CommandError(\"Error while deleting session: {}\"\n                                     .format(e))\n-    else:\n-        log.sessions.debug(\"Deleted session {}.\".format(name))\n+    log.sessions.debug(\"Deleted session {}.\".format(name))\n \n \n def load_default(name):\ndiff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py\nindex c7d93e76d..da463b647 100644\n--- a/qutebrowser/misc/split.py\n+++ b/qutebrowser/misc/split.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Our own fork of shlex.split with some added and removed features.\"\"\"\n \n@@ -203,10 +188,10 @@ def simple_split(s, keep=False, maxsplit=None):\n \n     if keep:\n         pattern = '([' + whitespace + '])'\n-        parts = re.split(pattern, s, maxsplit)\n+        parts = re.split(pattern, s, maxsplit=maxsplit)\n         return _combine_ws(parts, whitespace)\n     else:\n         pattern = '[' + whitespace + ']'\n-        parts = re.split(pattern, s, maxsplit)\n+        parts = re.split(pattern, s, maxsplit=maxsplit)\n         parts[-1] = parts[-1].rstrip()\n         return [p for p in parts if p]\ndiff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py\nindex 3a203f093..b23b862a3 100644\n--- a/qutebrowser/misc/sql.py\n+++ b/qutebrowser/misc/sql.py\n@@ -1,34 +1,20 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Ryan Roden-Corrent (rcorre) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Provides access to sqlite databases.\"\"\"\n \n+import enum\n import collections\n import contextlib\n import dataclasses\n import types\n-from typing import Any, Dict, Iterator, List, Mapping, MutableSequence, Optional, Type\n+from typing import Any, Dict, Iterator, List, Mapping, MutableSequence, Optional, Type, Union\n \n from qutebrowser.qt.core import QObject, pyqtSignal\n from qutebrowser.qt.sql import QSqlDatabase, QSqlError, QSqlQuery\n \n-from qutebrowser.qt import sip\n+from qutebrowser.qt import sip, machinery\n from qutebrowser.utils import debug, log\n \n \n@@ -69,24 +55,45 @@ class UserVersion:\n         return f'{self.major}.{self.minor}'\n \n \n-class SqliteErrorCode:\n+class SqliteErrorCode(enum.Enum):\n+    \"\"\"Primary error codes as used by sqlite.\n \n-    \"\"\"Error codes as used by sqlite.\n-\n-    See https://sqlite.org/rescode.html - note we only define the codes we use\n-    in qutebrowser here.\n+    See https://sqlite.org/rescode.html\n     \"\"\"\n \n-    ERROR = '1'  # generic error code\n-    BUSY = '5'  # database is locked\n-    READONLY = '8'  # attempt to write a readonly database\n-    IOERR = '10'  # disk I/O error\n-    CORRUPT = '11'  # database disk image is malformed\n-    FULL = '13'  # database or disk is full\n-    CANTOPEN = '14'  # unable to open database file\n-    PROTOCOL = '15'  # locking protocol error\n-    CONSTRAINT = '19'  # UNIQUE constraint failed\n-    NOTADB = '26'  # file is not a database\n+    # pylint: disable=invalid-name\n+\n+    OK = 0  # Successful result\n+    ERROR = 1  # Generic error\n+    INTERNAL = 2  # Internal logic error in SQLite\n+    PERM = 3  # Access permission denied\n+    ABORT = 4  # Callback routine requested an abort\n+    BUSY = 5  # The database file is locked\n+    LOCKED = 6  # A table in the database is locked\n+    NOMEM = 7  # A malloc() failed\n+    READONLY = 8  # Attempt to write a readonly database\n+    INTERRUPT = 9  # Operation terminated by sqlite3_interrupt()*/\n+    IOERR = 10  # Some kind of disk I/O error occurred\n+    CORRUPT = 11  # The database disk image is malformed\n+    NOTFOUND = 12  # Unknown opcode in sqlite3_file_control()\n+    FULL = 13  # Insertion failed because database is full\n+    CANTOPEN = 14  # Unable to open the database file\n+    PROTOCOL = 15  # Database lock protocol error\n+    EMPTY = 16  # Internal use only\n+    SCHEMA = 17  # The database schema changed\n+    TOOBIG = 18  # String or BLOB exceeds size limit\n+    CONSTRAINT = 19  # Abort due to constraint violation\n+    MISMATCH = 20  # Data type mismatch\n+    MISUSE = 21  # Library used incorrectly\n+    NOLFS = 22  # Uses OS features not supported on host\n+    AUTH = 23  # Authorization denied\n+    FORMAT = 24  # Not used\n+    RANGE = 25  # 2nd parameter to sqlite3_bind out of range\n+    NOTADB = 26  # File opened that is not a database file\n+    NOTICE = 27  # Notifications from sqlite3_log()\n+    WARNING = 28  # Warnings from sqlite3_log()\n+    ROW = 100  # sqlite3_step() has another row ready\n+    DONE = 101  # sqlite3_step() has finished executing\n \n \n class Error(Exception):\n@@ -104,8 +111,7 @@ class Error(Exception):\n         \"\"\"\n         if self.error is None:\n             return str(self)\n-        else:\n-            return self.error.databaseText()\n+        return self.error.databaseText()\n \n \n class KnownError(Error):\n@@ -128,6 +134,14 @@ class BugError(Error):\n def raise_sqlite_error(msg: str, error: QSqlError) -&gt; None:\n     \"\"\"Raise either a BugError or KnownError.\"\"\"\n     error_code = error.nativeErrorCode()\n+    primary_error_code: Union[SqliteErrorCode, str]\n+    try:\n+        # https://sqlite.org/rescode.html#pve\n+        primary_error_code = SqliteErrorCode(int(error_code) &amp; 0xff)\n+    except ValueError:\n+        # not an int, or unknown error code -&gt; fall back to string\n+        primary_error_code = error_code\n+\n     database_text = error.databaseText()\n     driver_text = error.driverText()\n \n@@ -135,7 +149,7 @@ def raise_sqlite_error(msg: str, error: QSqlError) -&gt; None:\n     log.sql.debug(f\"type: {debug.qenum_key(QSqlError, error.type())}\")\n     log.sql.debug(f\"database text: {database_text}\")\n     log.sql.debug(f\"driver text: {driver_text}\")\n-    log.sql.debug(f\"error code: {error_code}\")\n+    log.sql.debug(f\"error code: {error_code} -&gt; {primary_error_code}\")\n \n     known_errors = [\n         SqliteErrorCode.BUSY,\n@@ -151,12 +165,12 @@ def raise_sqlite_error(msg: str, error: QSqlError) -&gt; None:\n     # https://github.com/qutebrowser/qutebrowser/issues/4681\n     # If the query we built was too long\n     too_long_err = (\n-        error_code == SqliteErrorCode.ERROR and\n+        primary_error_code == SqliteErrorCode.ERROR and\n         (database_text.startswith(\"Expression tree is too large\") or\n          database_text in [\"too many SQL variables\",\n                            \"LIKE or GLOB pattern too complex\"]))\n \n-    if error_code in known_errors or too_long_err:\n+    if primary_error_code in known_errors or too_long_err:\n         raise KnownError(msg, error)\n \n     raise BugError(msg, error)\n@@ -299,13 +313,14 @@ class Query:\n         ok = self.query.prepare(querystr)\n         self._check_ok('prepare', ok)\n         self.query.setForwardOnly(forward_only)\n-        self.placeholders = []\n+        self._placeholders: List[str] = []\n \n     def __iter__(self) -&gt; Iterator[Any]:\n         if not self.query.isActive():\n             raise BugError(\"Cannot iterate inactive query\")\n         rec = self.query.record()\n         fields = [rec.fieldName(i) for i in range(rec.count())]\n+        # pylint: disable=prefer-typing-namedtuple\n         rowtype = collections.namedtuple(  # type: ignore[misc]\n             'ResultRow', fields)\n \n@@ -320,16 +335,26 @@ class Query:\n             msg = f'Failed to {step} query \"{query}\": \"{error.text()}\"'\n             raise_sqlite_error(msg, error)\n \n+    def _validate_bound_values(self) -&gt; None:\n+        \"\"\"Make sure all placeholders are bound.\"\"\"\n+        qt_bound_values = self.query.boundValues()\n+        if machinery.IS_QT5:\n+            # Qt 5: Returns a dict\n+            values = list(qt_bound_values.values())\n+        else:\n+            # Qt 6: Returns a list\n+            values = qt_bound_values\n+\n+        if None in values:\n+            raise BugError(\"Missing bound values!\")\n+\n     def _bind_values(self, values: Mapping[str, Any]) -&gt; Dict[str, Any]:\n-        self.placeholders = list(values)\n+        self._placeholders = list(values)\n         for key, val in values.items():\n             self.query.bindValue(f':{key}', val)\n \n-        bound_values = self.bound_values()\n-        if None in bound_values.values():\n-            raise BugError(\"Missing bound values!\")\n-\n-        return bound_values\n+        self._validate_bound_values()\n+        return self.bound_values()\n \n     def run(self, **values: Any) -&gt; 'Query':\n         \"\"\"Execute the prepared query.\"\"\"\n@@ -380,12 +405,10 @@ class Query:\n         return rows\n \n     def bound_values(self) -&gt; Dict[str, Any]:\n-        binds = {}\n-        for key in self.placeholders:\n-            key_s = f\":{key}\"\n-            val = self.query.boundValue(key_s)\n-            binds[key_s] = val\n-        return binds\n+        return {\n+            f\":{key}\": self.query.boundValue(f\":{key}\")\n+            for key in self._placeholders\n+        }\n \n \n class SqlTable(QObject):\ndiff --git a/qutebrowser/misc/throttle.py b/qutebrowser/misc/throttle.py\nindex 5e4dd13b2..43325fb08 100644\n--- a/qutebrowser/misc/throttle.py\n+++ b/qutebrowser/misc/throttle.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Jay Kamat \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Jay Kamat \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"A throttle for throttling function calls.\"\"\"\n \ndiff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py\nindex d94b32c26..6689ad074 100644\n--- a/qutebrowser/misc/utilcmds.py\n+++ b/qutebrowser/misc/utilcmds.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Misc. utility commands exposed to the user.\"\"\"\n \n@@ -41,9 +26,10 @@ from qutebrowser.utils.version import pastebin_version\n from qutebrowser.qt import sip\n \n \n-@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)\n+@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True,\n+                   deprecated_name='later')\n @cmdutils.argument('win_id', value=cmdutils.Value.win_id)\n-def later(duration: str, command: str, win_id: int) -&gt; None:\n+def cmd_later(duration: str, command: str, win_id: int) -&gt; None:\n     \"\"\"Execute a command after some time.\n \n     Args:\n@@ -72,10 +58,11 @@ def later(duration: str, command: str, win_id: int) -&gt; None:\n         raise\n \n \n-@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)\n+@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True,\n+                   deprecated_name='repeat')\n @cmdutils.argument('win_id', value=cmdutils.Value.win_id)\n @cmdutils.argument('count', value=cmdutils.Value.count)\n-def repeat(times: int, command: str, win_id: int, count: int = None) -&gt; None:\n+def cmd_repeat(times: int, command: str, win_id: int, count: int = None) -&gt; None:\n     \"\"\"Repeat a given command.\n \n     Args:\n@@ -93,14 +80,15 @@ def repeat(times: int, command: str, win_id: int, count: int = None) -&gt; None:\n         commandrunner.run_safely(command)\n \n \n-@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)\n+@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True,\n+                   deprecated_name='run-with-count')\n @cmdutils.argument('win_id', value=cmdutils.Value.win_id)\n @cmdutils.argument('count', value=cmdutils.Value.count)\n-def run_with_count(count_arg: int, command: str, win_id: int,\n+def cmd_run_with_count(count_arg: int, command: str, win_id: int,\n                    count: int = 1) -&gt; None:\n     \"\"\"Run a command with the given count.\n \n-    If run_with_count itself is run with a count, it multiplies count_arg.\n+    If cmd_run_with_count itself is run with a count, it multiplies count_arg.\n \n     Args:\n         count_arg: The count to pass to the command.\n@@ -199,10 +187,10 @@ def debug_set_fake_clipboard(s: str = None) -&gt; None:\n         utils.fake_clipboard = s\n \n \n-@cmdutils.register()\n+@cmdutils.register(deprecated_name='repeat-command')\n @cmdutils.argument('win_id', value=cmdutils.Value.win_id)\n @cmdutils.argument('count', value=cmdutils.Value.count)\n-def repeat_command(win_id: int, count: int = None) -&gt; None:\n+def cmd_repeat_last(win_id: int, count: int = None) -&gt; None:\n     \"\"\"Repeat the last executed command.\n \n     Args:\ndiff --git a/qutebrowser/qt/_core_pyqtproperty.py b/qutebrowser/qt/_core_pyqtproperty.py\nnew file mode 100644\nindex 000000000..c2078c403\n--- /dev/null\n+++ b/qutebrowser/qt/_core_pyqtproperty.py\n@@ -0,0 +1,78 @@\n+\"\"\"WORKAROUND for missing pyqtProperty typing, ported from PyQt5-stubs:\n+\n+FIXME:mypy PyQt6-stubs issue\n+https://github.com/python-qt-tools/PyQt5-stubs/blob/5.15.6.0/PyQt5-stubs/QtCore.pyi#L68-L111\n+\"\"\"\n+\n+# flake8: noqa\n+# pylint: disable=invalid-name,missing-class-docstring,too-many-arguments,redefined-builtin,unused-argument\n+\n+import typing\n+from PyQt6.QtCore import QObject, pyqtSignal\n+\n+if typing.TYPE_CHECKING:\n+    QObjectT = typing.TypeVar(\"QObjectT\", bound=QObject)\n+\n+    TPropertyTypeVal = typing.TypeVar(\"TPropertyTypeVal\")\n+\n+    TPropGetter = typing.TypeVar(\n+        \"TPropGetter\", bound=typing.Callable[[QObjectT], TPropertyTypeVal]\n+    )\n+    TPropSetter = typing.TypeVar(\n+        \"TPropSetter\", bound=typing.Callable[[QObjectT, TPropertyTypeVal], None]\n+    )\n+    TPropDeleter = typing.TypeVar(\n+        \"TPropDeleter\", bound=typing.Callable[[QObjectT], None]\n+    )\n+    TPropResetter = typing.TypeVar(\n+        \"TPropResetter\", bound=typing.Callable[[QObjectT], None]\n+    )\n+\n+    class pyqtProperty:\n+        def __init__(\n+            self,\n+            type: typing.Union[type, str],\n+            fget: typing.Optional[typing.Callable[[QObjectT], TPropertyTypeVal]] = None,\n+            fset: typing.Optional[\n+                typing.Callable[[QObjectT, TPropertyTypeVal], None]\n+            ] = None,\n+            freset: typing.Optional[typing.Callable[[QObjectT], None]] = None,\n+            fdel: typing.Optional[typing.Callable[[QObjectT], None]] = None,\n+            doc: typing.Optional[str] = \"\",\n+            designable: bool = True,\n+            scriptable: bool = True,\n+            stored: bool = True,\n+            user: bool = True,\n+            constant: bool = True,\n+            final: bool = True,\n+            notify: typing.Optional[pyqtSignal] = None,\n+            revision: int = 0,\n+        ) -&gt; None:\n+            ...\n+\n+        type: typing.Union[type, str]\n+        fget: typing.Optional[typing.Callable[[], TPropertyTypeVal]]\n+        fset: typing.Optional[typing.Callable[[TPropertyTypeVal], None]]\n+        freset: typing.Optional[typing.Callable[[], None]]\n+        fdel: typing.Optional[typing.Callable[[], None]]\n+\n+        def read(self, func: TPropGetter) -&gt; \"pyqtProperty\":\n+            ...\n+\n+        def write(self, func: TPropSetter) -&gt; \"pyqtProperty\":\n+            ...\n+\n+        def reset(self, func: TPropResetter) -&gt; \"pyqtProperty\":\n+            ...\n+\n+        def getter(self, func: TPropGetter) -&gt; \"pyqtProperty\":\n+            ...\n+\n+        def setter(self, func: TPropSetter) -&gt; \"pyqtProperty\":\n+            ...\n+\n+        def deleter(self, func: TPropDeleter) -&gt; \"pyqtProperty\":\n+            ...\n+\n+        def __call__(self, func: TPropGetter) -&gt; \"pyqtProperty\":\n+            ...\ndiff --git a/qutebrowser/qt/core.py b/qutebrowser/qt/core.py\nindex 5220d6389..87a253218 100644\n--- a/qutebrowser/qt/core.py\n+++ b/qutebrowser/qt/core.py\n@@ -1,17 +1,30 @@\n+# pylint: disable=import-error,wildcard-import,unused-wildcard-import\n+\n+\"\"\"Wrapped Qt imports for Qt Core.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qtcore-index.html\n+\"\"\"\n+\n+from typing import TYPE_CHECKING\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n \n-if machinery.USE_PYQT5:\n+\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtCore import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtCore import *\n-    Signal = pyqtSignal\n-    Slot = pyqtSlot\n elif machinery.USE_PYQT6:\n     from PyQt6.QtCore import *\n-    Signal = pyqtSignal\n-    Slot = pyqtSlot\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtCore import *\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtCore import *\n+\n+    if TYPE_CHECKING:\n+        from qutebrowser.qt._core_pyqtproperty import pyqtProperty\n else:\n     raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/dbus.py b/qutebrowser/qt/dbus.py\nindex 5bf21a35b..d3b22a747 100644\n--- a/qutebrowser/qt/dbus.py\n+++ b/qutebrowser/qt/dbus.py\n@@ -1,11 +1,26 @@\n+# pylint: disable=import-error,wildcard-import,unused-wildcard-import\n+\n+\"\"\"Wrapped Qt imports for Qt DBus.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qtdbus-index.html\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n+\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtDBus import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtDBus import *\n elif machinery.USE_PYQT6:\n     from PyQt6.QtDBus import *\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtDBus import *\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtDBus import *\n+else:\n+    raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/gui.py b/qutebrowser/qt/gui.py\nindex 9ce275217..dc5fbb23c 100644\n--- a/qutebrowser/qt/gui.py\n+++ b/qutebrowser/qt/gui.py\n@@ -1,17 +1,28 @@\n+# pylint: disable=import-error,wildcard-import,unused-wildcard-import,unused-import\n+\n+\"\"\"Wrapped Qt imports for Qt Gui.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qtgui-index.html\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n \n-if machinery.USE_PYQT5:\n+\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtGui import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtGui import *\n     from PyQt5.QtWidgets import QFileSystemModel\n     del QOpenGLVersionProfile  # moved to QtOpenGL in Qt 6\n elif machinery.USE_PYQT6:\n     from PyQt6.QtGui import *\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtGui import *\n-    from PySide2.QtWidgets import QFileSystemModel\n-    del QOpenGLVersionProfile  # moved to QtOpenGL in Qt 6\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtGui import *\n else:\n     raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/machinery.py b/qutebrowser/qt/machinery.py\nindex 3a4738779..45a1f6598 100644\n--- a/qutebrowser/qt/machinery.py\n+++ b/qutebrowser/qt/machinery.py\n@@ -1,67 +1,304 @@\n+# pyright: reportConstantRedefinition=false\n+\n+\"\"\"Qt wrapper selection.\n+\n+Contains selection logic and globals for Qt wrapper selection.\n+\n+All other files in this package are intended to be simple wrappers around Qt imports.\n+Depending on what is set in this module, they import from PyQt5 or PyQt6.\n+\n+The import wrappers are intended to be as thin as possible. They will not unify\n+API-level differences between Qt 5 and Qt 6. This is best handled by the calling code,\n+which has a better picture of what changed between APIs and how to best handle it.\n+\n+What they *will* do is handle simple 1:1 renames of classes, or moves between\n+modules (where they aim to always expose the Qt 6 API). See e.g. webenginecore.py.\n+\"\"\"\n+\n+# NOTE: No qutebrowser or PyQt import should be done here (at import time),\n+# as some early initialization needs to take place before that!\n+\n import os\n+import sys\n+import enum\n+import html\n+import argparse\n+import warnings\n import importlib\n+import dataclasses\n+from typing import Optional, Dict\n+\n+from qutebrowser.utils import log\n \n+# Packagers: Patch the line below to enforce a Qt wrapper, e.g.:\n+# sed -i 's/_WRAPPER_OVERRIDE = .*/_WRAPPER_OVERRIDE = \"PyQt6\"/' qutebrowser/qt/machinery.py\n+#\n+# Users: Set the QUTE_QT_WRAPPER environment variable to change the default wrapper.\n+_WRAPPER_OVERRIDE = None  # type: ignore[var-annotated]\n \n-_WRAPPERS = [\"PyQt6\", \"PyQt5\", \"PySide6\", \"PySide2\"]\n+WRAPPERS = [\n+    \"PyQt6\",\n+    \"PyQt5\",\n+    # Needs more work\n+    # \"PySide6\",\n+]\n \n \n class Error(Exception):\n-    pass\n+    \"\"\"Base class for all exceptions in this module.\"\"\"\n+\n+\n+class Unavailable(Error, ModuleNotFoundError):\n \n+    \"\"\"Raised when a module is unavailable with the given wrapper.\"\"\"\n \n-class Unavailable(Error, ImportError):\n-    pass\n+    def __init__(self) -&gt; None:\n+        super().__init__(f\"Unavailable with {INFO.wrapper}\")\n+\n+\n+class NoWrapperAvailableError(Error, ImportError):\n+\n+    \"\"\"Raised when no Qt wrapper is available.\"\"\"\n+\n+    def __init__(self, info: \"SelectionInfo\") -&gt; None:\n+        super().__init__(f\"No Qt wrapper was importable.\\n\\n{info}\")\n \n \n class UnknownWrapper(Error):\n-    pass\n+    \"\"\"Raised when an Qt module is imported but the wrapper values are unknown.\n+\n+    Should never happen (unless a new wrapper is added).\n+    \"\"\"\n+\n+\n+class SelectionReason(enum.Enum):\n+\n+    \"\"\"Reasons for selecting a Qt wrapper.\"\"\"\n+\n+    #: The wrapper was selected via --qt-wrapper.\n+    cli = \"--qt-wrapper\"\n \n+    #: The wrapper was selected via the QUTE_QT_WRAPPER environment variable.\n+    env = \"QUTE_QT_WRAPPER\"\n \n-def _autoselect_wrapper():\n-    for wrapper in _WRAPPERS:\n+    #: The wrapper was selected via autoselection.\n+    auto = \"autoselect\"\n+\n+    #: The default wrapper was selected.\n+    default = \"default\"\n+\n+    #: The wrapper was faked/patched out (e.g. in tests).\n+    fake = \"fake\"\n+\n+    #: The wrapper was overridden by patching _WRAPPER_OVERRIDE.\n+    override = \"override\"\n+\n+    #: The reason was not set.\n+    unknown = \"unknown\"\n+\n+\n+@dataclasses.dataclass\n+class SelectionInfo:\n+    \"\"\"Information about outcomes of importing Qt wrappers.\"\"\"\n+\n+    wrapper: Optional[str] = None\n+    outcomes: Dict[str, str] = dataclasses.field(default_factory=dict)\n+    reason: SelectionReason = SelectionReason.unknown\n+\n+    def set_module_error(self, name: str, error: Exception) -&gt; None:\n+        \"\"\"Set the outcome for a module import.\"\"\"\n+        self.outcomes[name] = f\"{type(error).__name__}: {error}\"\n+\n+    def use_wrapper(self, wrapper: str) -&gt; None:\n+        \"\"\"Set the wrapper to use.\"\"\"\n+        self.wrapper = wrapper\n+        self.outcomes[wrapper] = \"success\"\n+\n+    def __str__(self) -&gt; str:\n+        if not self.outcomes:\n+            # No modules were tried to be imported (no autoselection)\n+            # Thus, we can have a shorter output instead of adding noise.\n+            return f\"Qt wrapper: {self.wrapper} (via {self.reason.value})\"\n+\n+        lines = [\"Qt wrapper info:\"]\n+        for wrapper in WRAPPERS:\n+            outcome = self.outcomes.get(wrapper, \"not imported\")\n+            lines.append(f\"  {wrapper}: {outcome}\")\n+\n+        lines.append(f\"  -&gt; selected: {self.wrapper} (via {self.reason.value})\")\n+        return \"\\n\".join(lines)\n+\n+    def to_html(self) -&gt; str:\n+        return html.escape(str(self)).replace(\"\\n\", \"\")\n+\n+\n+def _autoselect_wrapper() -&gt; SelectionInfo:\n+    \"\"\"Autoselect a Qt wrapper.\n+\n+    This goes through all wrappers defined in WRAPPER.\n+    The first one which can be imported is returned.\n+    \"\"\"\n+    info = SelectionInfo(reason=SelectionReason.auto)\n+\n+    for wrapper in WRAPPERS:\n         try:\n             importlib.import_module(wrapper)\n-        except ImportError:\n-            # FIXME:qt6 show/log this somewhere?\n+        except ModuleNotFoundError as e:\n+            # Wrapper not available -&gt; try the next one.\n+            info.set_module_error(wrapper, e)\n             continue\n+        except ImportError as e:\n+            # Any other ImportError -&gt; stop to surface the error.\n+            info.set_module_error(wrapper, e)\n+            break\n+\n+        # Wrapper imported successfully -&gt; use it.\n+        info.use_wrapper(wrapper)\n+        return info\n \n-        # FIXME:qt6 what to do if none are available?\n-        return wrapper\n+    # SelectionInfo with wrapper=None but all error reports\n+    return info\n \n \n-def _select_wrapper():\n+def _select_wrapper(args: Optional[argparse.Namespace]) -&gt; SelectionInfo:\n+    \"\"\"Select a Qt wrapper.\n+\n+    - If --qt-wrapper is given, use that.\n+    - Otherwise, if the QUTE_QT_WRAPPER environment variable is set, use that.\n+    - Otherwise, try the wrappers in WRAPPER in order (PyQt6 -&gt; PyQt5)\n+    \"\"\"\n+    # If any Qt wrapper has been imported before this, something strange might\n+    # be happening. With PyInstaller, it imports the Qt bindings early.\n+    for name in WRAPPERS:\n+        if name in sys.modules and not hasattr(sys, \"frozen\"):\n+            warnings.warn(f\"{name} already imported\", stacklevel=1)\n+\n+    if args is not None and args.qt_wrapper is not None:\n+        assert args.qt_wrapper in WRAPPERS, args.qt_wrapper  # ensured by argparse\n+        return SelectionInfo(wrapper=args.qt_wrapper, reason=SelectionReason.cli)\n+\n     env_var = \"QUTE_QT_WRAPPER\"\n     env_wrapper = os.environ.get(env_var)\n-    if env_wrapper is None:\n-        return _autoselect_wrapper()\n-\n-    if env_wrapper not in _WRAPPERS:\n-        raise Error(f\"Unknown wrapper {env_wrapper} set via {env_var}, \"\n-                    f\"allowed: {', '.join(_WRAPPERS)}\")\n-\n-    return env_wrapper\n-\n-\n-WRAPPER = _select_wrapper()\n-USE_PYQT5 = WRAPPER == \"PyQt5\"\n-USE_PYQT6 = WRAPPER == \"PyQt6\"\n-USE_PYSIDE2 = WRAPPER == \"PySide2\"\n-USE_PYSIDE6 = WRAPPER == \"PySide6\"\n-assert USE_PYQT5 ^ USE_PYQT6 ^ USE_PYSIDE2 ^ USE_PYSIDE6\n-\n-IS_QT5 = USE_PYQT5 or USE_PYSIDE2\n-IS_QT6 = USE_PYQT6 or USE_PYSIDE6\n-IS_PYQT = USE_PYQT5 or USE_PYQT6\n-IS_PYSIDE = USE_PYSIDE2 or USE_PYSIDE6\n-assert IS_QT5 ^ IS_QT6\n-assert IS_PYQT ^ IS_PYSIDE\n-\n-\n-if USE_PYQT5:\n-    PACKAGE = \"PyQt5\"\n-elif USE_PYQT6:\n-    PACKAGE = \"PyQt6\"\n-elif USE_PYSIDE2:\n-    PACKAGE = \"PySide2\"\n-elif USE_PYSIDE6:\n-    PACKAGE = \"PySide6\"\n+    if env_wrapper:\n+        if env_wrapper == \"auto\":\n+            return _autoselect_wrapper()\n+        elif env_wrapper not in WRAPPERS:\n+            raise Error(\n+                f\"Unknown wrapper {env_wrapper} set via {env_var}, \"\n+                f\"allowed: {', '.join(WRAPPERS)}\"\n+            )\n+        return SelectionInfo(wrapper=env_wrapper, reason=SelectionReason.env)\n+\n+    if _WRAPPER_OVERRIDE is not None:\n+        assert _WRAPPER_OVERRIDE in WRAPPERS\n+        return SelectionInfo(wrapper=_WRAPPER_OVERRIDE, reason=SelectionReason.override)\n+\n+    return _autoselect_wrapper()\n+\n+\n+# Values are set in init(). If you see a NameError here, it means something tried to\n+# import Qt (or check for its availability) before machinery.init() was called.\n+\n+#: Information about the wrapper that ended up being selected.\n+#: Should not be used directly, use one of the USE_* or IS_* constants below\n+#: instead, as those are supported by type checking.\n+INFO: SelectionInfo\n+\n+#: Whether we're using PyQt5. Consider using IS_QT5 or IS_PYQT instead.\n+USE_PYQT5: bool\n+\n+#: Whether we're using PyQt6. Consider using IS_QT6 or IS_PYQT instead.\n+USE_PYQT6: bool\n+\n+#: Whether we're using PySide6. Consider using IS_QT6 or IS_PYSIDE instead.\n+USE_PYSIDE6: bool\n+\n+#: Whether we are using any Qt 5 wrapper.\n+IS_QT5: bool\n+\n+#: Whether we are using any Qt 6 wrapper.\n+IS_QT6: bool\n+\n+#: Whether we are using any PyQt wrapper.\n+IS_PYQT: bool\n+\n+#: Whether we are using any PySide wrapper.\n+IS_PYSIDE: bool\n+\n+_initialized = False\n+\n+\n+def _set_globals(info: SelectionInfo) -&gt; None:\n+    \"\"\"Set all global variables in this module based on the given SelectionInfo.\n+\n+    Those are split into multiple global variables because that way we can teach mypy\n+    about them via --always-true and --always-false, see tox.ini.\n+    \"\"\"\n+    global INFO, USE_PYQT5, USE_PYQT6, USE_PYSIDE6, IS_QT5, IS_QT6, IS_PYQT, IS_PYSIDE, _initialized\n+\n+    assert info.wrapper is not None, info\n+    assert not _initialized\n+\n+    _initialized = True\n+    INFO = info\n+    USE_PYQT5 = info.wrapper == \"PyQt5\"\n+    USE_PYQT6 = info.wrapper == \"PyQt6\"\n+    USE_PYSIDE6 = info.wrapper == \"PySide6\"\n+    assert USE_PYQT5 + USE_PYQT6 + USE_PYSIDE6 == 1\n+\n+    IS_QT5 = USE_PYQT5\n+    IS_QT6 = USE_PYQT6 or USE_PYSIDE6\n+    IS_PYQT = USE_PYQT5 or USE_PYQT6\n+    IS_PYSIDE = USE_PYSIDE6\n+    assert IS_QT5 ^ IS_QT6\n+    assert IS_PYQT ^ IS_PYSIDE\n+\n+\n+def init_implicit() -&gt; None:\n+    \"\"\"Initialize Qt wrapper globals implicitly at Qt import time.\n+\n+    This gets called when any qutebrowser.qt module is imported, and implicitly\n+    initializes the Qt wrapper globals.\n+\n+    After this is called, no explicit initialization via machinery.init() is possible\n+    anymore - thus, this should never be called before init() when running qutebrowser\n+    as an application (and any further calls will be a no-op).\n+\n+    However, this ensures that any qutebrowser module can be imported without\n+    having to worry about machinery.init().  This is useful for e.g. tests or\n+    manual interactive usage of the qutebrowser code.\n+    \"\"\"\n+    if _initialized:\n+        # Implicit initialization can happen multiple times\n+        # (all subsequent calls are a no-op)\n+        return\n+\n+    info = _select_wrapper(args=None)\n+    if info.wrapper is None:\n+        raise NoWrapperAvailableError(info)\n+\n+    _set_globals(info)\n+\n+\n+def init(args: argparse.Namespace) -&gt; SelectionInfo:\n+    \"\"\"Initialize Qt wrapper globals during qutebrowser application start.\n+\n+    This gets called from earlyinit.py, i.e. after we have an argument parser,\n+    but before any kinds of Qt usage. This allows `args` to be passed, which is\n+    used to select the Qt wrapper (if --qt-wrapper is given).\n+\n+    If any qutebrowser.qt module is imported before this, init_implicit() will be called\n+    instead, which means this can't be called anymore.\n+    \"\"\"\n+    if _initialized:\n+        raise Error(\"init() already called before application init\")\n+\n+    info = _select_wrapper(args)\n+    if info.wrapper is not None:\n+        _set_globals(info)\n+        log.init.debug(str(info))\n+\n+    # If info is None here (no Qt wrapper available), we'll show an error later\n+    # in earlyinit.py.\n+\n+    return info\ndiff --git a/qutebrowser/qt/network.py b/qutebrowser/qt/network.py\nindex 44b1778e3..7b194affc 100644\n--- a/qutebrowser/qt/network.py\n+++ b/qutebrowser/qt/network.py\n@@ -1,13 +1,26 @@\n+# pylint: disable=import-error,wildcard-import,unused-wildcard-import\n+\n+\"\"\"Wrapped Qt imports for Qt Network.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qtnetwork-index.html\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n+\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtNetwork import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtNetwork import *\n elif machinery.USE_PYQT6:\n     from PyQt6.QtNetwork import *\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtNetwork import *\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtNetwork import *\n else:\n     raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/opengl.py b/qutebrowser/qt/opengl.py\nindex e08a47601..bc5a31c11 100644\n--- a/qutebrowser/qt/opengl.py\n+++ b/qutebrowser/qt/opengl.py\n@@ -1,13 +1,26 @@\n+# pylint: disable=import-error,wildcard-import,unused-import,unused-wildcard-import\n+\n+\"\"\"Wrapped Qt imports for Qt OpenGL.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qtopengl-index.html\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n+\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtOpenGL import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtGui import QOpenGLVersionProfile\n elif machinery.USE_PYQT6:\n     from PyQt6.QtOpenGL import *\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtGui import QOpenGLVersionProfile\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtOpenGL import *\n else:\n     raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/printsupport.py b/qutebrowser/qt/printsupport.py\nindex 04a0d7334..08358d417 100644\n--- a/qutebrowser/qt/printsupport.py\n+++ b/qutebrowser/qt/printsupport.py\n@@ -1,11 +1,26 @@\n+# pylint: disable=import-error,wildcard-import,unused-wildcard-import\n+\n+\"\"\"Wrapped Qt imports for Qt Print Support.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qtprintsupport-index.html\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n+\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtPrintSupport import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtPrintSupport import *\n elif machinery.USE_PYQT6:\n     from PyQt6.QtPrintSupport import *\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtPrintSupport import *\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtPrintSupport import *\n+else:\n+    raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/qml.py b/qutebrowser/qt/qml.py\nindex 85bcd0157..9202667e2 100644\n--- a/qutebrowser/qt/qml.py\n+++ b/qutebrowser/qt/qml.py\n@@ -1,11 +1,26 @@\n+# pylint: disable=import-error,wildcard-import,unused-wildcard-import\n+\n+\"\"\"Wrapped Qt imports for Qt QML.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qtqml-index.html\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n+\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtQml import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtQml import *\n elif machinery.USE_PYQT6:\n     from PyQt6.QtQml import *\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtQml import *\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtQml import *\n+else:\n+    raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/sip.py b/qutebrowser/qt/sip.py\nindex 9930a879e..1eb21bc27 100644\n--- a/qutebrowser/qt/sip.py\n+++ b/qutebrowser/qt/sip.py\n@@ -1,21 +1,36 @@\n+# pylint: disable=wildcard-import,unused-wildcard-import\n+\n+\"\"\"Wrapped Qt imports for PyQt5.sip/PyQt6.sip.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/sip directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the PyQt6.sip API:\n+https://www.riverbankcomputing.com/static/Docs/PyQt6/api/sip/sip-module.html\n+\n+Note that we don't yet abstract between PySide/PyQt here.\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n-# While upstream recommends using PyQt6.sip ever since PyQt6 5.11, some distributions\n-# still package later versions of PyQt6 with a top-level \"sip\" rather than \"PyQt6.sip\".\n+machinery.init_implicit()\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:  # pylint: disable=no-else-raise\n+    raise machinery.Unavailable()\n+elif machinery.USE_PYQT5:\n     try:\n         from PyQt5.sip import *\n     except ImportError:\n-        from sip import *\n+        from sip import *  # type: ignore[import-not-found]\n elif machinery.USE_PYQT6:\n     try:\n         from PyQt6.sip import *\n     except ImportError:\n-        from sip import *\n-elif machinery.USE_PYSIDE2:\n-    raise machinery.Unavailable()\n-elif machinery.USE_PYSIDE6:\n-    raise machinery.Unavailable()\n+        # While upstream recommends using PyQt5.sip ever since PyQt5 5.11, some\n+        # distributions still package later versions of PyQt5 with a top-level\n+        # \"sip\" rather than \"PyQt5.sip\".\n+        from sip import *  # type: ignore[import-not-found]\n else:\n     raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/sql.py b/qutebrowser/qt/sql.py\nindex e50753e2c..4d969936b 100644\n--- a/qutebrowser/qt/sql.py\n+++ b/qutebrowser/qt/sql.py\n@@ -1,11 +1,26 @@\n+# pylint: disable=import-error,wildcard-import,unused-wildcard-import\n+\n+\"\"\"Wrapped Qt imports for Qt SQL.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qtsql-index.html\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n+\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtSql import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtSql import *\n elif machinery.USE_PYQT6:\n     from PyQt6.QtSql import *\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtSql import *\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtSql import *\n+else:\n+    raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/test.py b/qutebrowser/qt/test.py\nindex 5d3d02cb0..3c1bcfdff 100644\n--- a/qutebrowser/qt/test.py\n+++ b/qutebrowser/qt/test.py\n@@ -1,11 +1,26 @@\n+# pylint: disable=import-error,wildcard-import,unused-wildcard-import\n+\n+\"\"\"Wrapped Qt imports for Qt Test.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qttest-index.html\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n+\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtTest import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtTest import *\n elif machinery.USE_PYQT6:\n     from PyQt6.QtTest import *\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtTest import *\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtTest import *\n+else:\n+    raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/webenginecore.py b/qutebrowser/qt/webenginecore.py\nindex a5e1f6d0a..afd76e38c 100644\n--- a/qutebrowser/qt/webenginecore.py\n+++ b/qutebrowser/qt/webenginecore.py\n@@ -1,7 +1,24 @@\n+# pylint: disable=import-error,wildcard-import,unused-wildcard-import,unused-import\n+\n+\"\"\"Wrapped Qt imports for Qt WebEngine Core.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qtwebenginecore-index.html\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n+\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtWebEngineCore import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtWebEngineCore import *\n     from PyQt5.QtWebEngineWidgets import (\n         QWebEngineSettings,\n@@ -11,23 +28,14 @@ if machinery.USE_PYQT5:\n         QWebEngineCertificateError,\n         QWebEngineScript,\n         QWebEngineHistory,\n+        QWebEngineHistoryItem,\n+        QWebEngineScriptCollection,\n+        QWebEngineClientCertificateSelection,\n+        QWebEngineFullScreenRequest,\n+        QWebEngineContextMenuData as QWebEngineContextMenuRequest,\n     )\n-    # FIXME:qt6 is there a PySide2 equivalent to those?\n     from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION, PYQT_WEBENGINE_VERSION_STR\n elif machinery.USE_PYQT6:\n     from PyQt6.QtWebEngineCore import *\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtWebEngineCore import *\n-    from PySide2.QtWebEngineWidgets import (\n-        QWebEngineSettings,\n-        QWebEngineProfile,\n-        QWebEngineDownloadItem as QWebEngineDownloadRequest,\n-        QWebEnginePage,\n-        QWebEngineCertificateError,\n-        QWebEngineScript,\n-        QWebEngineHistory,\n-    )\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtWebEngineCore import *\n else:\n     raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/webenginewidgets.py b/qutebrowser/qt/webenginewidgets.py\nindex 4b7031496..88758cf23 100644\n--- a/qutebrowser/qt/webenginewidgets.py\n+++ b/qutebrowser/qt/webenginewidgets.py\n@@ -1,19 +1,33 @@\n+# pylint: disable=import-error,wildcard-import,unused-wildcard-import\n+\n+\"\"\"Wrapped Qt imports for Qt WebEngine Widgets.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qtwebenginewidgets-index.html\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n+\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtWebEngineWidgets import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtWebEngineWidgets import *\n elif machinery.USE_PYQT6:\n     from PyQt6.QtWebEngineWidgets import *\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtWebEngineWidgets import *\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtWebEngineWidgets import *\n else:\n     raise machinery.UnknownWrapper()\n \n \n if machinery.IS_QT5:\n+    # pylint: disable=undefined-variable\n     # moved to WebEngineCore in Qt 6\n     del QWebEngineSettings\n     del QWebEngineProfile\n@@ -22,3 +36,8 @@ if machinery.IS_QT5:\n     del QWebEngineCertificateError\n     del QWebEngineScript\n     del QWebEngineHistory\n+    del QWebEngineHistoryItem\n+    del QWebEngineScriptCollection\n+    del QWebEngineClientCertificateSelection\n+    del QWebEngineFullScreenRequest\n+    del QWebEngineContextMenuData\ndiff --git a/qutebrowser/qt/webkit.py b/qutebrowser/qt/webkit.py\nindex 15f2bbf98..c4b0bb7ae 100644\n--- a/qutebrowser/qt/webkit.py\n+++ b/qutebrowser/qt/webkit.py\n@@ -1,13 +1,33 @@\n+# pylint: disable=wildcard-import\n+\n+\"\"\"Wrapped Qt imports for Qt WebKit.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6\n+(though WebKit is only supported with Qt 5).\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the QtWebKit 5.212 API:\n+https://qtwebkit.github.io/doc/qtwebkit/qtwebkit-index.html\n+\"\"\"\n+\n+import typing\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n \n-if machinery.USE_PYQT5:\n+\n+if machinery.USE_PYSIDE6:  # pylint: disable=no-else-raise\n+    raise machinery.Unavailable()\n+elif machinery.USE_PYQT5 or typing.TYPE_CHECKING:\n+    # If we use mypy (even on Qt 6), we pretend to have WebKit available.\n+    # This avoids central API (like BrowserTab) being Any because the webkit part of\n+    # the unions there is missing.\n+    # This causes various issues inside browser/webkit/, but we ignore those in\n+    # .mypy.ini because we don't really care much about QtWebKit anymore.\n     from PyQt5.QtWebKit import *\n elif machinery.USE_PYQT6:\n     raise machinery.Unavailable()\n-elif machinery.USE_PYSIDE2:\n-    raise machinery.Unavailable()\n-elif machinery.USE_PYSIDE6:\n-    raise machinery.Unavailable()\n else:\n     raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/webkitwidgets.py b/qutebrowser/qt/webkitwidgets.py\nindex 3ae74c3c9..5b790dcc7 100644\n--- a/qutebrowser/qt/webkitwidgets.py\n+++ b/qutebrowser/qt/webkitwidgets.py\n@@ -1,13 +1,34 @@\n+# pylint: disable=wildcard-import,no-else-raise\n+\n+\"\"\"Wrapped Qt imports for Qt WebKit Widgets.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6\n+(though WebKit is only supported with Qt 5).\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the QtWebKit 5.212 API:\n+https://qtwebkit.github.io/doc/qtwebkit/qtwebkitwidgets-index.html\n+\"\"\"\n+\n+import typing\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n+\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:\n+    raise machinery.Unavailable()\n+elif machinery.USE_PYQT5 or typing.TYPE_CHECKING:\n+    # If we use mypy (even on Qt 6), we pretend to have WebKit available.\n+    # This avoids central API (like BrowserTab) being Any because the webkit part of\n+    # the unions there is missing.\n+    # This causes various issues inside browser/webkit/, but we ignore those in\n+    # .mypy.ini because we don't really care much about QtWebKit anymore.\n     from PyQt5.QtWebKitWidgets import *\n elif machinery.USE_PYQT6:\n     raise machinery.Unavailable()\n-elif machinery.USE_PYSIDE2:\n-    raise machinery.Unavailable()\n-elif machinery.USE_PYSIDE6:\n-    raise machinery.Unavailable()\n else:\n     raise machinery.UnknownWrapper()\ndiff --git a/qutebrowser/qt/widgets.py b/qutebrowser/qt/widgets.py\nindex 5fb4ee92b..f82ec2e3b 100644\n--- a/qutebrowser/qt/widgets.py\n+++ b/qutebrowser/qt/widgets.py\n@@ -1,14 +1,30 @@\n+# pylint: disable=import-error,wildcard-import,unused-wildcard-import\n+\n+\"\"\"Wrapped Qt imports for Qt Widgets.\n+\n+All code in qutebrowser should use this module instead of importing from\n+PyQt/PySide directly. This allows supporting both Qt 5 and Qt 6.\n+\n+See machinery.py for details on how Qt wrapper selection works.\n+\n+Any API exported from this module is based on the Qt 6 API:\n+https://doc.qt.io/qt-6/qtwidgets-index.html\n+\"\"\"\n+\n from qutebrowser.qt import machinery\n \n+machinery.init_implicit()\n+\n \n-if machinery.USE_PYQT5:\n+if machinery.USE_PYSIDE6:\n+    from PySide6.QtWidgets import *\n+elif machinery.USE_PYQT5:\n     from PyQt5.QtWidgets import *\n elif machinery.USE_PYQT6:\n     from PyQt6.QtWidgets import *\n-elif machinery.USE_PYSIDE2:\n-    from PySide2.QtWidgets import *\n-elif machinery.USE_PYSIDE6:\n-    from PySide6.QtWidgets import *\n+else:\n+    raise machinery.UnknownWrapper()\n \n if machinery.IS_QT5:\n+    # pylint: disable=undefined-variable\n     del QFileSystemModel  # moved to QtGui in Qt 6\ndiff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py\nindex c576c4a06..e68156759 100644\n--- a/qutebrowser/qutebrowser.py\n+++ b/qutebrowser/qutebrowser.py\n@@ -1,21 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Early initialization and main entry point.\n \n@@ -52,8 +38,9 @@ except ImportError:\n         sys.exit(100)\n check_python_version()\n \n-import argparse  # pylint: disable=wrong-import-order\n+import argparse\n from qutebrowser.misc import earlyinit\n+from qutebrowser.qt import machinery\n \n \n def get_argparser():\n@@ -82,11 +69,16 @@ def get_argparser():\n                              \"qutebrowser instance running.\")\n     parser.add_argument('--backend', choices=['webkit', 'webengine'],\n                         help=\"Which backend to use.\")\n+    parser.add_argument('--qt-wrapper', choices=machinery.WRAPPERS,\n+                        help=\"Which Qt wrapper to use. This can also be set \"\n+                        \"via the QUTE_QT_WRAPPER environment variable. \"\n+                        \"If both are set, the command line argument takes \"\n+                        \"precedence.\")\n     parser.add_argument('--desktop-file-name',\n                         default=\"org.qutebrowser.qutebrowser\",\n                         help=\"Set the base name of the desktop entry for this \"\n                         \"application. Used to set the app_id under Wayland. See \"\n-                        \"https://doc.qt.io/qt-5/qguiapplication.html#desktopFileName-prop\")\n+                        \"https://doc.qt.io/qt-6/qguiapplication.html#desktopFileName-prop\")\n     parser.add_argument('--untrusted-args',\n                         action='store_true',\n                         help=\"Mark all following arguments as untrusted, which \"\n@@ -98,12 +90,6 @@ def get_argparser():\n                         help=argparse.SUPPRESS,\n                         action='store_true')\n \n-    # WORKAROUND to be able to restart from older qutebrowser versions into this one.\n-    # Should be removed at some point.\n-    parser.add_argument('--enable-webengine-inspector',\n-                        help=argparse.SUPPRESS,\n-                        action='store_true')\n-\n     debug = parser.add_argument_group('debug arguments')\n     debug.add_argument('-l', '--loglevel', dest='loglevel',\n                        help=\"Override the configured console loglevel\",\n@@ -185,12 +171,13 @@ def debug_flag_error(flag):\n         avoid-chromium-init: Enable `--version` without initializing Chromium.\n         werror: Turn Python warnings into errors.\n         test-notification-service: Use the testing libnotify service.\n+        caret: Enable debug logging for caret.js.\n     \"\"\"\n     valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history',\n                    'no-scroll-filtering', 'log-requests', 'log-cookies',\n                    'log-scroll-pos', 'log-sensitive-keys', 'stack', 'chromium',\n                    'wait-renderer-process', 'avoid-chromium-init', 'werror',\n-                   'test-notification-service']\n+                   'test-notification-service', 'log-qt-events', 'caret']\n \n     if flag in valid_flags:\n         return flag\ndiff --git a/qutebrowser/utils/__init__.py b/qutebrowser/utils/__init__.py\nindex 3f6a4f239..8baa20f45 100644\n--- a/qutebrowser/utils/__init__.py\n+++ b/qutebrowser/utils/__init__.py\n@@ -1,20 +1,5 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Misc utility functions.\"\"\"\ndiff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py\nindex 156a56c07..433e2274f 100644\n--- a/qutebrowser/utils/debug.py\n+++ b/qutebrowser/utils/debug.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utilities used for debugging.\"\"\"\n \n@@ -33,7 +18,7 @@ from qutebrowser.qt.core import Qt, QEvent, QMetaMethod, QObject, pyqtBoundSigna\n \n from qutebrowser.utils import log, utils, qtutils, objreg\n from qutebrowser.misc import objects\n-from qutebrowser.qt import sip\n+from qutebrowser.qt import sip, machinery\n \n \n def log_events(klass: Type[QObject]) -&gt; Type[QObject]:\n@@ -43,8 +28,10 @@ def log_events(klass: Type[QObject]) -&gt; Type[QObject]:\n     @functools.wraps(old_event)\n     def new_event(self: Any, e: QEvent) -&gt; bool:\n         \"\"\"Wrapper for event() which logs events.\"\"\"\n-        log.misc.debug(\"Event in {}: {}\".format(utils.qualname(klass),\n-                                                qenum_key(QEvent, e.type())))\n+        # Passing klass as a WORKAROUND because with PyQt6, QEvent.type() returns int:\n+        # https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044583.html\n+        log.misc.debug(\"Event in {}: {}\".format(\n+            utils.qualname(klass), qenum_key(QEvent, e.type(), klass=QEvent.Type)))\n         return old_event(self, e)\n \n     klass.event = new_event  # type: ignore[assignment]\n@@ -68,6 +55,7 @@ def log_signals(obj: QObject) -&gt; QObject:\n     def connect_log_slot(obj: QObject) -&gt; None:\n         \"\"\"Helper function to connect all signals to a logging slot.\"\"\"\n         metaobj = obj.metaObject()\n+        assert metaobj is not None\n         for i in range(metaobj.methodCount()):\n             meta_method = metaobj.method(i)\n             qtutils.ensure_valid(meta_method)\n@@ -97,19 +85,68 @@ def log_signals(obj: QObject) -&gt; QObject:\n     return obj\n \n \n-_EnumValueType = Union[sip.simplewrapper, int]\n+if machinery.IS_QT6:\n+    _EnumValueType = Union[enum.Enum, int]\n+else:\n+    _EnumValueType = Union[sip.simplewrapper, int]\n+\n+\n+def _qenum_key_python(\n+    value: _EnumValueType,\n+    klass: Type[_EnumValueType],\n+) -&gt; Optional[str]:\n+    \"\"\"New-style PyQt6: Try getting value from Python enum.\"\"\"\n+    if isinstance(value, enum.Enum) and value.name:\n+        return value.name\n+\n+    # We got an int with klass passed: Try asking Python enum for member\n+    if issubclass(klass, enum.Enum):\n+        try:\n+            assert isinstance(value, int)\n+            name = klass(value).name\n+            if name is not None and name != str(value):\n+                return name\n+        except ValueError:\n+            pass\n+\n+    return None\n+\n+\n+def _qenum_key_qt(\n+    base: Type[sip.simplewrapper],\n+    value: _EnumValueType,\n+    klass: Type[_EnumValueType],\n+) -&gt; Optional[str]:\n+    # On PyQt5, or PyQt6 with int passed: Try to ask Qt's introspection.\n+    # However, not every Qt enum value has a staticMetaObject\n+    try:\n+        meta_obj = base.staticMetaObject  # type: ignore[attr-defined]\n+        idx = meta_obj.indexOfEnumerator(klass.__name__)\n+        meta_enum = meta_obj.enumerator(idx)\n+        key = meta_enum.valueToKey(int(value))  # type: ignore[arg-type]\n+        if key is not None:\n+            return key\n+    except AttributeError:\n+        pass\n \n+    # PyQt5: Try finding value match in class\n+    for name, obj in vars(base).items():\n+        if isinstance(obj, klass) and obj == value:\n+            return name\n \n-def qenum_key(base: Type[_EnumValueType],\n-              value: _EnumValueType,\n-              add_base: bool = False,\n-              klass: Type[_EnumValueType] = None) -&gt; str:\n+    return None\n+\n+\n+def qenum_key(\n+    base: Type[sip.simplewrapper],\n+    value: _EnumValueType,\n+    klass: Type[_EnumValueType] = None,\n+) -&gt; str:\n     \"\"\"Convert a Qt Enum value to its key as a string.\n \n     Args:\n         base: The object the enum is in, e.g. QFrame.\n         value: The value to get.\n-        add_base: Whether the base should be added to the printed name.\n         klass: The enum class the value belongs to.\n                If None, the class will be auto-guessed.\n \n@@ -117,40 +154,26 @@ def qenum_key(base: Type[_EnumValueType],\n         The key associated with the value as a string if it could be found.\n         The original value as a string if not.\n     \"\"\"\n-    if isinstance(value, enum.Enum):\n-        # New-style PyQt 6\n-        return value.name\n-\n     if klass is None:\n         klass = value.__class__\n         if klass == int:\n             raise TypeError(\"Can't guess enum class of an int!\")\n+    assert klass is not None\n \n-    try:\n-        meta_obj = base.staticMetaObject  # type: ignore[union-attr]\n-        idx = meta_obj.indexOfEnumerator(klass.__name__)\n-        meta_enum = meta_obj.enumerator(idx)\n-        ret = meta_enum.valueToKey(int(value))  # type: ignore[arg-type]\n-    except AttributeError:\n-        ret = None\n+    name = _qenum_key_python(value=value, klass=klass)\n+    if name is not None:\n+        return name\n \n-    if ret is None:\n-        for name, obj in vars(base).items():\n-            if isinstance(obj, klass) and obj == value:\n-                ret = name\n-                break\n-        else:\n-            ret = '0x{:04x}'.format(int(value))  # type: ignore[arg-type]\n+    name = _qenum_key_qt(base=base, value=value, klass=klass)\n+    if name is not None:\n+        return name\n \n-    if add_base and hasattr(base, '__name__'):\n-        return '.'.join([base.__name__, ret])\n-    else:\n-        return ret\n+    # Last resort fallback: Hex value\n+    return '0x{:04x}'.format(int(value))  # type: ignore[arg-type]\n \n \n-def qflags_key(base: Type[_EnumValueType],\n+def qflags_key(base: Type[sip.simplewrapper],\n                value: _EnumValueType,\n-               add_base: bool = False,\n                klass: Type[_EnumValueType] = None) -&gt; str:\n     \"\"\"Convert a Qt QFlags value to its keys as string.\n \n@@ -162,7 +185,6 @@ def qflags_key(base: Type[_EnumValueType],\n     Args:\n         base: The object the flags are in, e.g. QtCore.Qt\n         value: The value to get.\n-        add_base: Whether the base should be added to the printed names.\n         klass: The flags class the value belongs to.\n                If None, the class will be auto-guessed.\n \n@@ -170,13 +192,6 @@ def qflags_key(base: Type[_EnumValueType],\n         The keys associated with the flags as a '|' separated string if they\n         could be found. Hex values as a string if not.\n     \"\"\"\n-    if isinstance(value, enum.Flag):\n-        # New-style PyQt 6\n-        if value.name is None:\n-            # FIXME:qt6 can we do something better here?\n-            return repr(value)\n-        return value.name\n-\n     if klass is None:\n         # We have to store klass here because it will be lost when iterating\n         # over the bits.\n@@ -185,21 +200,21 @@ def qflags_key(base: Type[_EnumValueType],\n             raise TypeError(\"Can't guess enum class of an int!\")\n \n     if not value:\n-        return qenum_key(base, value, add_base, klass)\n+        return qenum_key(base, value, klass)\n \n     bits = []\n     names = []\n     mask = 0x01\n-    value = int(value)  # type: ignore[arg-type]\n-    while mask &lt;= value:\n-        if value &amp; mask:\n+    intval = qtutils.extract_enum_val(value)\n+    while mask &lt;= intval:\n+        if intval &amp; mask:\n             bits.append(mask)\n         mask &lt;&lt;= 1\n     for bit in bits:\n         # We have to re-convert to an enum type here or we'll sometimes get an\n         # empty string back.\n-        enum_value = klass(bit)  # type: ignore[call-arg]\n-        names.append(qenum_key(base, enum_value, add_base))\n+        enum_value = klass(bit)  # type: ignore[call-arg,unused-ignore]\n+        names.append(qenum_key(base, enum_value, klass))\n     return '|'.join(names)\n \n \ndiff --git a/qutebrowser/utils/docutils.py b/qutebrowser/utils/docutils.py\nindex 89e799c89..6cd16730c 100644\n--- a/qutebrowser/utils/docutils.py\n+++ b/qutebrowser/utils/docutils.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utilities used for the documentation and built-in help.\"\"\"\n \ndiff --git a/qutebrowser/utils/error.py b/qutebrowser/utils/error.py\nindex 3a155a4fe..010970861 100644\n--- a/qutebrowser/utils/error.py\n+++ b/qutebrowser/utils/error.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tools related to error printing/displaying.\"\"\"\n \ndiff --git a/qutebrowser/utils/javascript.py b/qutebrowser/utils/javascript.py\nindex 2850db3d1..9890be446 100644\n--- a/qutebrowser/utils/javascript.py\n+++ b/qutebrowser/utils/javascript.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utilities related to javascript interaction.\"\"\"\n \ndiff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py\nindex 5775b317b..d7c261942 100644\n--- a/qutebrowser/utils/jinja.py\n+++ b/qutebrowser/utils/jinja.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utilities related to jinja2.\"\"\"\n \n@@ -114,7 +99,7 @@ class Environment(jinja2.Environment):\n         url = QUrl('qute://resource')\n         url.setPath('/' + path)\n         urlutils.ensure_valid(url)\n-        urlstr = url.toString(QUrl.ComponentFormattingOption.FullyEncoded)  # type: ignore[arg-type]\n+        urlstr = url.toString(urlutils.FormatOption.ENCODED)\n         return urlstr\n \n     def _data_url(self, path: str) -&gt; str:\n@@ -142,7 +127,7 @@ js_environment = jinja2.Environment(loader=Loader('javascript'))\n \n \n @debugcachestats.register()\n-@functools.lru_cache()\n+@functools.lru_cache\n def template_config_variables(template: str) -&gt; FrozenSet[str]:\n     \"\"\"Return the config variables used in the template.\"\"\"\n     unvisted_nodes: List[jinja2.nodes.Node] = [environment.parse(template)]\ndiff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py\nindex 32bd7129c..9695ec5a2 100644\n--- a/qutebrowser/utils/log.py\n+++ b/qutebrowser/utils/log.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Loggers and utilities related to logging.\"\"\"\n \n@@ -26,28 +11,27 @@ import logging\n import contextlib\n import collections\n import copy\n-import faulthandler\n-import traceback\n import warnings\n import json\n import inspect\n import argparse\n from typing import (TYPE_CHECKING, Any, Iterator, Mapping, MutableSequence,\n-                    Optional, Set, Tuple, Union)\n+                    Optional, Set, Tuple, Union, TextIO, Literal, cast)\n+\n+# NOTE: This is a Qt-free zone! All imports related to Qt logging should be done in\n+# qutebrowser.utils.qtlog (see https://github.com/qutebrowser/qutebrowser/issues/7769).\n \n-from qutebrowser.qt import core as QtCore\n # Optional imports\n try:\n     import colorama\n except ImportError:\n-    colorama = None\n+    colorama = None  # type: ignore[assignment]\n \n if TYPE_CHECKING:\n     from qutebrowser.config import config as configmodule\n-    from typing import TextIO\n \n _log_inited = False\n-_args = None\n+_args: Optional[argparse.Namespace] = None\n \n COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'purple', 'cyan', 'white']\n COLOR_ESCAPES = {color: '\\033[{}m'.format(i)\n@@ -162,7 +146,7 @@ LOGGER_NAMES = [\n \n ram_handler: Optional['RAMHandler'] = None\n console_handler: Optional[logging.Handler] = None\n-console_filter = None\n+console_filter: Optional[\"LogFilter\"] = None\n \n \n def stub(suffix: str = '') -&gt; None:\n@@ -211,15 +195,9 @@ def init_log(args: argparse.Namespace) -&gt; None:\n     root.setLevel(logging.NOTSET)\n     logging.captureWarnings(True)\n     _init_py_warnings()\n-    QtCore.qInstallMessageHandler(qt_message_handler)\n     _log_inited = True\n \n \n-@QtCore.pyqtSlot()\n-def shutdown_log() -&gt; None:\n-    QtCore.qInstallMessageHandler(None)\n-\n-\n def _init_py_warnings() -&gt; None:\n     \"\"\"Initialize Python warning handling.\"\"\"\n     assert _args is not None\n@@ -231,24 +209,26 @@ def _init_py_warnings() -&gt; None:\n                             message=r\"Using or importing the ABCs from \"\n                             r\"'collections' instead of from 'collections.abc' \"\n                             r\"is deprecated.*\")\n+    # PyQt 5.15/6.2/6.3/6.4:\n+    # https://riverbankcomputing.com/news/SIP_v6.7.12_Released\n+    warnings.filterwarnings(\n+        'ignore',\n+        category=DeprecationWarning,\n+        message=(\n+            r\"sipPyTypeDict\\(\\) is deprecated, the extension module should use \"\n+            r\"sipPyTypeDictRef\\(\\) instead\"\n+        )\n+    )\n \n \n @contextlib.contextmanager\n-def disable_qt_msghandler() -&gt; Iterator[None]:\n-    \"\"\"Contextmanager which temporarily disables the Qt message handler.\"\"\"\n-    old_handler = QtCore.qInstallMessageHandler(None)\n-    try:\n-        yield\n-    finally:\n-        QtCore.qInstallMessageHandler(old_handler)\n-\n-\n-@contextlib.contextmanager\n-def py_warning_filter(action: str = 'ignore', **kwargs: Any) -&gt; Iterator[None]:\n+def py_warning_filter(\n+    action:\n+        Literal['default', 'error', 'ignore', 'always', 'module', 'once'] = 'ignore',\n+    **kwargs: Any,\n+) -&gt; Iterator[None]:\n     \"\"\"Contextmanager to temporarily disable certain Python warnings.\"\"\"\n-    # FIXME Use Literal['default', 'error', 'ignore', 'always', 'module', 'once']\n-    # once we use Python 3.8 or typing_extensions\n-    warnings.filterwarnings(action, **kwargs)  # type: ignore[arg-type]\n+    warnings.filterwarnings(action, **kwargs)\n     yield\n     if _log_inited:\n         _init_py_warnings()\n@@ -279,7 +259,7 @@ def _init_handlers(\n     else:\n         strip = False if force_color else None\n         if use_colorama:\n-            stream = colorama.AnsiToWin32(sys.stderr, strip=strip)\n+            stream = cast(TextIO, colorama.AnsiToWin32(sys.stderr, strip=strip))\n         else:\n             stream = sys.stderr\n         console_handler = logging.StreamHandler(stream)\n@@ -378,162 +358,6 @@ def change_console_formatter(level: int) -&gt; None:\n         assert isinstance(old_formatter, JSONFormatter), old_formatter\n \n \n-def qt_message_handler(msg_type: QtCore.QtMsgType,\n-                       context: QtCore.QMessageLogContext,\n-                       msg: str) -&gt; None:\n-    \"\"\"Qt message handler to redirect qWarning etc. to the logging system.\n-\n-    Args:\n-        msg_type: The level of the message.\n-        context: The source code location of the message.\n-        msg: The message text.\n-    \"\"\"\n-    # Mapping from Qt logging levels to the matching logging module levels.\n-    # Note we map critical to ERROR as it's actually \"just\" an error, and fatal\n-    # to critical.\n-    qt_to_logging = {\n-        QtCore.QtMsgType.QtDebugMsg: logging.DEBUG,\n-        QtCore.QtMsgType.QtWarningMsg: logging.WARNING,\n-        QtCore.QtMsgType.QtCriticalMsg: logging.ERROR,\n-        QtCore.QtMsgType.QtFatalMsg: logging.CRITICAL,\n-    }\n-    try:\n-        qt_to_logging[QtCore.QtMsgType.QtInfoMsg] = logging.INFO\n-    except AttributeError:\n-        # Added in Qt 5.5.\n-        # While we don't support Qt &lt; 5.5 anymore, logging still needs to work so that\n-        # the Qt version warning in earlyinit.py does.\n-        pass\n-\n-    # Change levels of some well-known messages to debug so they don't get\n-    # shown to the user.\n-    #\n-    # If a message starts with any text in suppressed_msgs, it's not logged as\n-    # error.\n-    suppressed_msgs = [\n-        # PNGs in Qt with broken color profile\n-        # https://bugreports.qt.io/browse/QTBUG-39788\n-        ('libpng warning: iCCP: Not recognizing known sRGB profile that has '\n-         'been edited'),\n-        'libpng warning: iCCP: known incorrect sRGB profile',\n-        # Hopefully harmless warning\n-        'OpenType support missing for script ',\n-        # Error if a QNetworkReply gets two different errors set. Harmless Qt\n-        # bug on some pages.\n-        # https://bugreports.qt.io/browse/QTBUG-30298\n-        ('QNetworkReplyImplPrivate::error: Internal problem, this method must '\n-         'only be called once.'),\n-        # Sometimes indicates missing text, but most of the time harmless\n-        'load glyph failed ',\n-        # Harmless, see https://bugreports.qt.io/browse/QTBUG-42479\n-        ('content-type missing in HTTP POST, defaulting to '\n-         'application/x-www-form-urlencoded. '\n-         'Use QNetworkRequest::setHeader() to fix this problem.'),\n-        # https://bugreports.qt.io/browse/QTBUG-43118\n-        'Using blocking call!',\n-        # Hopefully harmless\n-        ('\"Method \"GetAll\" with signature \"s\" on interface '\n-         '\"org.freedesktop.DBus.Properties\" doesn\\'t exist'),\n-        ('\"Method \\\\\"GetAll\\\\\" with signature \\\\\"s\\\\\" on interface '\n-         '\\\\\"org.freedesktop.DBus.Properties\\\\\" doesn\\'t exist\\\\n\"'),\n-        'WOFF support requires QtWebKit to be built with zlib support.',\n-        # Weird Enlightment/GTK X extensions\n-        'QXcbWindow: Unhandled client message: \"_E_',\n-        'QXcbWindow: Unhandled client message: \"_ECORE_',\n-        'QXcbWindow: Unhandled client message: \"_GTK_',\n-        # Happens on AppVeyor CI\n-        'SetProcessDpiAwareness failed:',\n-        # https://bugreports.qt.io/browse/QTBUG-49174\n-        ('QObject::connect: Cannot connect (null)::stateChanged('\n-         'QNetworkSession::State) to '\n-         'QNetworkReplyHttpImpl::_q_networkSessionStateChanged('\n-         'QNetworkSession::State)'),\n-        # https://bugreports.qt.io/browse/QTBUG-53989\n-        (\"Image of format '' blocked because it is not considered safe. If \"\n-         \"you are sure it is safe to do so, you can white-list the format by \"\n-         \"setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=\"),\n-        # Installing Qt from the installer may cause it looking for SSL3 or\n-        # OpenSSL 1.0 which may not be available on the system\n-        \"QSslSocket: cannot resolve \",\n-        \"QSslSocket: cannot call unresolved function \",\n-        # When enabling debugging with QtWebEngine\n-        (\"Remote debugging server started successfully. Try pointing a \"\n-         \"Chromium-based browser to \"),\n-        # https://github.com/qutebrowser/qutebrowser/issues/1287\n-        \"QXcbClipboard: SelectionRequest too old\",\n-        # https://github.com/qutebrowser/qutebrowser/issues/2071\n-        'QXcbWindow: Unhandled client message: \"\"',\n-        # https://codereview.qt-project.org/176831\n-        \"QObject::disconnect: Unexpected null parameter\",\n-        # https://bugreports.qt.io/browse/QTBUG-76391\n-        \"Attribute Qt::AA_ShareOpenGLContexts must be set before \"\n-        \"QCoreApplication is created.\",\n-    ]\n-    # not using utils.is_mac here, because we can't be sure we can successfully\n-    # import the utils module here.\n-    if sys.platform == 'darwin':\n-        suppressed_msgs += [\n-            # https://bugreports.qt.io/browse/QTBUG-47154\n-            ('virtual void QSslSocketBackendPrivate::transmit() SSLRead '\n-             'failed with: -9805'),\n-        ]\n-\n-    if not msg:\n-        msg = \"Logged empty message!\"\n-\n-    if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs):\n-        level = logging.DEBUG\n-    else:\n-        level = qt_to_logging[msg_type]\n-\n-    if context.line is None:\n-        lineno = -1  # type: ignore[unreachable]\n-    else:\n-        lineno = context.line\n-\n-    if context.function is None:\n-        func = 'none'  # type: ignore[unreachable]\n-    elif ':' in context.function:\n-        func = '\"{}\"'.format(context.function)\n-    else:\n-        func = context.function\n-\n-    if context.category is None or context.category == 'default':\n-        name = 'qt'\n-    else:\n-        name = 'qt-' + context.category\n-    if msg.splitlines()[0] == ('This application failed to start because it '\n-                               'could not find or load the Qt platform plugin '\n-                               '\"xcb\".'):\n-        # Handle this message specially.\n-        msg += (\"\\n\\nOn Archlinux, this should fix the problem:\\n\"\n-                \"    pacman -S libxkbcommon-x11\")\n-        faulthandler.disable()\n-\n-    assert _args is not None\n-    if _args.debug:\n-        stack: Optional[str] = ''.join(traceback.format_stack())\n-    else:\n-        stack = None\n-\n-    record = qt.makeRecord(name=name, level=level, fn=context.file, lno=lineno,\n-                           msg=msg, args=(), exc_info=None, func=func,\n-                           sinfo=stack)\n-    qt.handle(record)\n-\n-\n-@contextlib.contextmanager\n-def hide_qt_warning(pattern: str, logger: str = 'qt') -&gt; Iterator[None]:\n-    \"\"\"Hide Qt warnings matching the given regex.\"\"\"\n-    log_filter = QtWarningFilter(pattern)\n-    logger_obj = logging.getLogger(logger)\n-    logger_obj.addFilter(log_filter)\n-    try:\n-        yield\n-    finally:\n-        logger_obj.removeFilter(log_filter)\n-\n-\n def init_from_config(conf: 'configmodule.ConfigContainer') -&gt; None:\n     \"\"\"Initialize logging settings from the config.\n \n@@ -564,24 +388,6 @@ def init_from_config(conf: 'configmodule.ConfigContainer') -&gt; None:\n             change_console_formatter(level)\n \n \n-class QtWarningFilter(logging.Filter):\n-\n-    \"\"\"Filter to filter Qt warnings.\n-\n-    Attributes:\n-        _pattern: The start of the message.\n-    \"\"\"\n-\n-    def __init__(self, pattern: str) -&gt; None:\n-        super().__init__()\n-        self._pattern = pattern\n-\n-    def filter(self, record: logging.LogRecord) -&gt; bool:\n-        \"\"\"Determine if the specified record is to be logged.\"\"\"\n-        do_log = not record.msg.strip().startswith(self._pattern)\n-        return do_log\n-\n-\n class InvalidLogFilterError(Exception):\n \n     \"\"\"Raised when an invalid filter string is passed to LogFilter.parse().\"\"\"\n@@ -728,10 +534,10 @@ class ColoredFormatter(logging.Formatter):\n \n     def __init__(self, fmt: str,\n                  datefmt: str,\n-                 style: str, *,\n+                 style: Literal[\"%\", \"{\", \"$\"],\n+                 *,\n                  use_colors: bool) -&gt; None:\n-        # FIXME Use Literal[\"%\", \"{\", \"$\"] once we use Python 3.8 or typing_extensions\n-        super().__init__(fmt, datefmt, style)  # type: ignore[arg-type]\n+        super().__init__(fmt, datefmt, style)\n         self.use_colors = use_colors\n \n     def format(self, record: logging.LogRecord) -&gt; str:\ndiff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py\nindex 10183afee..275ed2f3d 100644\n--- a/qutebrowser/utils/message.py\n+++ b/qutebrowser/utils/message.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n # Because every method needs to have a log_stack argument\n # and because we use *args a lot\n@@ -150,7 +135,7 @@ def ask(*args: Any, **kwargs: Any) -&gt; Any:\n     \"\"\"Ask a modular question in the statusbar (blocking).\n \n     Args:\n-        message: The message to display to the user.\n+        title: The message to display to the user.\n         mode: A PromptMode.\n         default: The default value to display.\n         text: Additional text to show\n@@ -197,7 +182,7 @@ def confirm_async(*, yes_action: _ActionType,\n     \"\"\"Ask a yes/no question to the user and execute the given actions.\n \n     Args:\n-        message: The message to display to the user.\n+        title: The message to display to the user.\n         yes_action: Callable to be called when the user answered yes.\n         no_action: Callable to be called when the user answered no.\n         cancel_action: Callable to be called when the user cancelled the\ndiff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py\nindex 3b6823df4..c0715d90a 100644\n--- a/qutebrowser/utils/objreg.py\n+++ b/qutebrowser/utils/objreg.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The global object registry and related utility functions.\"\"\"\n \n@@ -170,7 +155,7 @@ def _get_tab_registry(win_id: _WindowTab,\n         window: Optional[QWidget] = QApplication.activeWindow()\n         if window is None or not hasattr(window, 'win_id'):\n             raise RegistryUnavailableError('tab')\n-        win_id = window.win_id  # type: ignore[attr-defined]\n+        win_id = window.win_id\n     elif win_id is None:\n         raise TypeError(\"window is None with scope tab!\")\n \ndiff --git a/qutebrowser/utils/qtlog.py b/qutebrowser/utils/qtlog.py\nnew file mode 100644\nindex 000000000..78b48ebee\n--- /dev/null\n+++ b/qutebrowser/utils/qtlog.py\n@@ -0,0 +1,215 @@\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n+\"\"\"Loggers and utilities related to Qt logging.\"\"\"\n+\n+import argparse\n+import contextlib\n+import faulthandler\n+import logging\n+import sys\n+import traceback\n+from typing import Iterator, Optional\n+\n+from qutebrowser.qt import core as qtcore\n+from qutebrowser.utils import log\n+\n+_args: Optional[argparse.Namespace] = None\n+\n+\n+def init(args: argparse.Namespace) -&gt; None:\n+    \"\"\"Install Qt message handler based on the argparse namespace passed.\"\"\"\n+    global _args\n+    _args = args\n+    qtcore.qInstallMessageHandler(qt_message_handler)\n+\n+\n+@qtcore.pyqtSlot()\n+def shutdown_log() -&gt; None:\n+    qtcore.qInstallMessageHandler(None)\n+\n+\n+@contextlib.contextmanager\n+def disable_qt_msghandler() -&gt; Iterator[None]:\n+    \"\"\"Contextmanager which temporarily disables the Qt message handler.\"\"\"\n+    old_handler = qtcore.qInstallMessageHandler(None)\n+    try:\n+        yield\n+    finally:\n+        qtcore.qInstallMessageHandler(old_handler)\n+\n+\n+def qt_message_handler(msg_type: qtcore.QtMsgType,\n+                       context: qtcore.QMessageLogContext,\n+                       msg: Optional[str]) -&gt; None:\n+    \"\"\"Qt message handler to redirect qWarning etc. to the logging system.\n+\n+    Args:\n+        msg_type: The level of the message.\n+        context: The source code location of the message.\n+        msg: The message text.\n+    \"\"\"\n+    # Mapping from Qt logging levels to the matching logging module levels.\n+    # Note we map critical to ERROR as it's actually \"just\" an error, and fatal\n+    # to critical.\n+    qt_to_logging = {\n+        qtcore.QtMsgType.QtDebugMsg: logging.DEBUG,\n+        qtcore.QtMsgType.QtWarningMsg: logging.WARNING,\n+        qtcore.QtMsgType.QtCriticalMsg: logging.ERROR,\n+        qtcore.QtMsgType.QtFatalMsg: logging.CRITICAL,\n+        qtcore.QtMsgType.QtInfoMsg: logging.INFO,\n+    }\n+\n+    # Change levels of some well-known messages to debug so they don't get\n+    # shown to the user.\n+    #\n+    # If a message starts with any text in suppressed_msgs, it's not logged as\n+    # error.\n+    suppressed_msgs = [\n+        # PNGs in Qt with broken color profile\n+        # https://bugreports.qt.io/browse/QTBUG-39788\n+        ('libpng warning: iCCP: Not recognizing known sRGB profile that has '\n+         'been edited'),\n+        'libpng warning: iCCP: known incorrect sRGB profile',\n+        # Hopefully harmless warning\n+        'OpenType support missing for script ',\n+        # Error if a QNetworkReply gets two different errors set. Harmless Qt\n+        # bug on some pages.\n+        # https://bugreports.qt.io/browse/QTBUG-30298\n+        ('QNetworkReplyImplPrivate::error: Internal problem, this method must '\n+         'only be called once.'),\n+        # Sometimes indicates missing text, but most of the time harmless\n+        'load glyph failed ',\n+        # Harmless, see https://bugreports.qt.io/browse/QTBUG-42479\n+        ('content-type missing in HTTP POST, defaulting to '\n+         'application/x-www-form-urlencoded. '\n+         'Use QNetworkRequest::setHeader() to fix this problem.'),\n+        # https://bugreports.qt.io/browse/QTBUG-43118\n+        'Using blocking call!',\n+        # Hopefully harmless\n+        ('\"Method \"GetAll\" with signature \"s\" on interface '\n+         '\"org.freedesktop.DBus.Properties\" doesn\\'t exist'),\n+        ('\"Method \\\\\"GetAll\\\\\" with signature \\\\\"s\\\\\" on interface '\n+         '\\\\\"org.freedesktop.DBus.Properties\\\\\" doesn\\'t exist\\\\n\"'),\n+        'WOFF support requires QtWebKit to be built with zlib support.',\n+        # Weird Enlightment/GTK X extensions\n+        'QXcbWindow: Unhandled client message: \"_E_',\n+        'QXcbWindow: Unhandled client message: \"_ECORE_',\n+        'QXcbWindow: Unhandled client message: \"_GTK_',\n+        # Happens on AppVeyor CI\n+        'SetProcessDpiAwareness failed:',\n+        # https://bugreports.qt.io/browse/QTBUG-49174\n+        ('QObject::connect: Cannot connect (null)::stateChanged('\n+         'QNetworkSession::State) to '\n+         'QNetworkReplyHttpImpl::_q_networkSessionStateChanged('\n+         'QNetworkSession::State)'),\n+        # https://bugreports.qt.io/browse/QTBUG-53989\n+        (\"Image of format '' blocked because it is not considered safe. If \"\n+         \"you are sure it is safe to do so, you can white-list the format by \"\n+         \"setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=\"),\n+        # Installing Qt from the installer may cause it looking for SSL3 or\n+        # OpenSSL 1.0 which may not be available on the system\n+        \"QSslSocket: cannot resolve \",\n+        \"QSslSocket: cannot call unresolved function \",\n+        # When enabling debugging with QtWebEngine\n+        (\"Remote debugging server started successfully. Try pointing a \"\n+         \"Chromium-based browser to \"),\n+        # https://github.com/qutebrowser/qutebrowser/issues/1287\n+        \"QXcbClipboard: SelectionRequest too old\",\n+        # https://github.com/qutebrowser/qutebrowser/issues/2071\n+        'QXcbWindow: Unhandled client message: \"\"',\n+        # https://codereview.qt-project.org/176831\n+        \"QObject::disconnect: Unexpected null parameter\",\n+        # https://bugreports.qt.io/browse/QTBUG-76391\n+        \"Attribute Qt::AA_ShareOpenGLContexts must be set before \"\n+        \"QCoreApplication is created.\",\n+        # Qt 6.4 beta 1: https://bugreports.qt.io/browse/QTBUG-104741\n+        \"GL format 0 is not supported\",\n+    ]\n+    # not using utils.is_mac here, because we can't be sure we can successfully\n+    # import the utils module here.\n+    if sys.platform == 'darwin':\n+        suppressed_msgs += [\n+            # https://bugreports.qt.io/browse/QTBUG-47154\n+            ('virtual void QSslSocketBackendPrivate::transmit() SSLRead '\n+             'failed with: -9805'),\n+        ]\n+\n+    if not msg:\n+        msg = \"Logged empty message!\"\n+\n+    if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs):\n+        level = logging.DEBUG\n+    elif context.category == \"qt.webenginecontext\" and (\n+        msg.strip().startswith(\"GL Type: \") or  # Qt 6.3\n+        msg.strip().startswith(\"GLImplementation:\")  # Qt 6.2\n+    ):\n+        level = logging.DEBUG\n+    else:\n+        level = qt_to_logging[msg_type]\n+\n+    if context.line is None:\n+        lineno = -1  # type: ignore[unreachable]\n+    else:\n+        lineno = context.line\n+\n+    if context.function is None:\n+        func = 'none'  # type: ignore[unreachable]\n+    elif ':' in context.function:\n+        func = '\"{}\"'.format(context.function)\n+    else:\n+        func = context.function\n+\n+    if context.category is None or context.category == 'default':\n+        name = 'qt'\n+    else:\n+        name = 'qt-' + context.category\n+    if msg.splitlines()[0] == ('This application failed to start because it '\n+                               'could not find or load the Qt platform plugin '\n+                               '\"xcb\".'):\n+        # Handle this message specially.\n+        msg += (\"\\n\\nOn Archlinux, this should fix the problem:\\n\"\n+                \"    pacman -S libxkbcommon-x11\")\n+        faulthandler.disable()\n+\n+    assert _args is not None\n+    if _args.debug:\n+        stack: Optional[str] = ''.join(traceback.format_stack())\n+    else:\n+        stack = None\n+\n+    record = log.qt.makeRecord(name=name, level=level, fn=context.file, lno=lineno,\n+                           msg=msg, args=(), exc_info=None, func=func,\n+                           sinfo=stack)\n+    log.qt.handle(record)\n+\n+\n+class QtWarningFilter(logging.Filter):\n+\n+    \"\"\"Filter to filter Qt warnings.\n+\n+    Attributes:\n+        _pattern: The start of the message.\n+    \"\"\"\n+\n+    def __init__(self, pattern: str) -&gt; None:\n+        super().__init__()\n+        self._pattern = pattern\n+\n+    def filter(self, record: logging.LogRecord) -&gt; bool:\n+        \"\"\"Determine if the specified record is to be logged.\"\"\"\n+        do_log = not record.msg.strip().startswith(self._pattern)\n+        return do_log\n+\n+\n+@contextlib.contextmanager\n+def hide_qt_warning(pattern: str, logger: str = 'qt') -&gt; Iterator[None]:\n+    \"\"\"Hide Qt warnings matching the given regex.\"\"\"\n+    log_filter = QtWarningFilter(pattern)\n+    logger_obj = logging.getLogger(logger)\n+    logger_obj.addFilter(log_filter)\n+    try:\n+        yield\n+    finally:\n+        logger_obj.removeFilter(log_filter)\ndiff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py\nindex 47ba418d1..c1f05b78d 100644\n--- a/qutebrowser/utils/qtutils.py\n+++ b/qutebrowser/utils/qtutils.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Misc. utilities related to Qt.\n \n@@ -33,9 +18,10 @@ import enum\n import pathlib\n import operator\n import contextlib\n-from typing import (Any, AnyStr, TYPE_CHECKING, BinaryIO, IO, Iterator,\n-                    Optional, Union, Tuple, cast)\n+from typing import (Any, TYPE_CHECKING, BinaryIO, IO, Iterator, Literal,\n+                    Optional, Union, Tuple, Protocol, cast, overload, TypeVar)\n \n+from qutebrowser.qt import machinery, sip\n from qutebrowser.qt.core import (qVersion, QEventLoop, QDataStream, QByteArray,\n                           QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR,\n                           PYQT_VERSION_STR, QObject, QUrl, QLibraryInfo)\n@@ -46,7 +32,8 @@ except ImportError:  # pragma: no cover\n     qWebKitVersion = None  # type: ignore[assignment]  # noqa: N816\n if TYPE_CHECKING:\n     from qutebrowser.qt.webkit import QWebHistory\n-    from qutebrowser.qt.webenginewidgets import QWebEngineHistory\n+    from qutebrowser.qt.webenginecore import QWebEngineHistory\n+    from typing_extensions import TypeGuard  # added in Python 3.10\n \n from qutebrowser.misc import objects\n from qutebrowser.utils import usertypes, utils\n@@ -93,17 +80,36 @@ def version_check(version: str,\n                   compiled: bool = True) -&gt; bool:\n     \"\"\"Check if the Qt runtime version is the version supplied or newer.\n \n+    By default this function will check `version` against:\n+\n+    1. the runtime Qt version (from qVersion())\n+    2. the Qt version that PyQt was compiled against (from QT_VERSION_STR)\n+    3. the PyQt version (from PYQT_VERSION_STR)\n+\n+    With `compiled=False` only the runtime Qt version (1) is checked.\n+\n+    You can often run older PyQt versions against newer Qt versions, but you\n+    won't be able to access any APIs that were only added in the newer Qt\n+    version. So if you want to check if a new feature is supported, use the\n+    default behavior. If you just want to check the underlying Qt version,\n+    pass `compiled=False`.\n+\n     Args:\n         version: The version to check against.\n         exact: if given, check with == instead of &gt;=\n-        compiled: Set to False to not check the compiled version.\n+        compiled: Set to False to not check the compiled Qt version or the\n+          PyQt version.\n     \"\"\"\n     if compiled and exact:\n         raise ValueError(\"Can't use compiled=True with exact=True!\")\n \n     parsed = utils.VersionNumber.parse(version)\n     op = operator.eq if exact else operator.ge\n-    result = op(utils.VersionNumber.parse(qVersion()), parsed)\n+\n+    qversion = qVersion()\n+    assert qversion is not None\n+    result = op(utils.VersionNumber.parse(qversion), parsed)\n+\n     if compiled and result:\n         # qVersion() ==/&gt;= parsed, now check if QT_VERSION_STR ==/&gt;= parsed.\n         result = op(utils.VersionNumber.parse(QT_VERSION_STR), parsed)\n@@ -132,6 +138,11 @@ def is_single_process() -&gt; bool:\n     return '--single-process' in args\n \n \n+def is_wayland() -&gt; bool:\n+    \"\"\"Check if we are running on Wayland.\"\"\"\n+    return objects.qapp.platformName() in [\"wayland\", \"wayland-egl\"]\n+\n+\n def check_overflow(arg: int, ctype: str, fatal: bool = True) -&gt; int:\n     \"\"\"Check if the given argument is in bounds for the given type.\n \n@@ -158,7 +169,7 @@ def check_overflow(arg: int, ctype: str, fatal: bool = True) -&gt; int:\n         return arg\n \n \n-class Validatable(utils.Protocol):\n+class Validatable(Protocol):\n \n     \"\"\"An object with an isValid() method (e.g. QUrl).\"\"\"\n \n@@ -182,6 +193,16 @@ def check_qdatastream(stream: QDataStream) -&gt; None:\n         QDataStream.Status.WriteFailed: (\"The data stream cannot write to the \"\n                                   \"underlying device.\"),\n     }\n+    if machinery.IS_QT6:\n+        try:\n+            status_to_str[QDataStream.Status.SizeLimitExceeded] = (\n+                \"The data stream cannot read or write the data because its size is larger \"\n+                \"than supported by the current platform.\"\n+            )\n+        except AttributeError:\n+            # Added in Qt 6.7\n+            pass\n+\n     if stream.status() != QDataStream.Status.Ok:\n         raise OSError(status_to_str[stream.status()])\n \n@@ -225,12 +246,32 @@ def deserialize_stream(stream: QDataStream, obj: _QtSerializableType) -&gt; None:\n     check_qdatastream(stream)\n \n \n+@overload\n+@contextlib.contextmanager\n+def savefile_open(\n+        filename: str,\n+        binary: Literal[False] = ...,\n+        encoding: str = 'utf-8'\n+) -&gt; Iterator[IO[str]]:\n+    ...\n+\n+\n+@overload\n+@contextlib.contextmanager\n+def savefile_open(\n+        filename: str,\n+        binary: Literal[True],\n+        encoding: str = 'utf-8'\n+) -&gt; Iterator[IO[bytes]]:\n+    ...\n+\n+\n @contextlib.contextmanager\n def savefile_open(\n         filename: str,\n         binary: bool = False,\n         encoding: str = 'utf-8'\n-) -&gt; Iterator[IO[AnyStr]]:\n+) -&gt; Iterator[Union[IO[str], IO[bytes]]]:\n     \"\"\"Context manager to easily use a QSaveFile.\"\"\"\n     f = QSaveFile(filename)\n     cancelled = False\n@@ -242,7 +283,7 @@ def savefile_open(\n         dev = cast(BinaryIO, PyQIODevice(f))\n \n         if binary:\n-            new_f: IO[Any] = dev  # FIXME:mypy Why doesn't AnyStr work?\n+            new_f: Union[IO[str], IO[bytes]] = dev\n         else:\n             new_f = io.TextIOWrapper(dev, encoding=encoding)\n \n@@ -369,7 +410,7 @@ class PyQIODevice(io.BufferedIOBase):\n         self._check_readable()\n \n         if size is None or size &lt; 0:\n-            qt_size = 0  # no maximum size\n+            qt_size = None  # no maximum size\n         elif size == 0:\n             return b''\n         else:\n@@ -377,7 +418,10 @@ class PyQIODevice(io.BufferedIOBase):\n \n         buf: Union[QByteArray, bytes, None] = None\n         if self.dev.canReadLine():\n-            buf = self.dev.readLine(qt_size)\n+            if qt_size is None:\n+                buf = self.dev.readLine()\n+            else:\n+                buf = self.dev.readLine(qt_size)\n         elif size is None or size &lt; 0:\n             buf = self.dev.readAll()\n         else:\n@@ -452,6 +496,13 @@ class QtValueError(ValueError):\n         super().__init__(err)\n \n \n+if machinery.IS_QT6:\n+    _ProcessEventFlagType = QEventLoop.ProcessEventsFlag\n+else:\n+    _ProcessEventFlagType = Union[\n+        QEventLoop.ProcessEventsFlag, QEventLoop.ProcessEventsFlags]\n+\n+\n class EventLoop(QEventLoop):\n \n     \"\"\"A thin wrapper around QEventLoop.\n@@ -464,14 +515,15 @@ class EventLoop(QEventLoop):\n         self._executing = False\n \n     def exec(\n-            self,\n-            flags: QEventLoop.ProcessEventsFlag =\n-            QEventLoop.ProcessEventsFlag.AllEvents\n+        self,\n+        flags: _ProcessEventFlagType = QEventLoop.ProcessEventsFlag.AllEvents,\n     ) -&gt; int:\n         \"\"\"Override exec_ to raise an exception when re-running.\"\"\"\n         if self._executing:\n             raise AssertionError(\"Eventloop is already running!\")\n         self._executing = True\n+        if machinery.IS_QT5:\n+            flags = cast(QEventLoop.ProcessEventsFlags, flags)\n         status = super().exec(flags)\n         self._executing = False\n         return status\n@@ -525,24 +577,58 @@ def interpolate_color(\n \n     if colorspace is None:\n         if percent == 100:\n-            return QColor(*end.getRgb())\n+            r, g, b, a = end.getRgb()\n+            assert r is not None\n+            assert g is not None\n+            assert b is not None\n+            assert a is not None\n+            return QColor(r, g, b, a)\n         else:\n-            return QColor(*start.getRgb())\n+            r, g, b, a = start.getRgb()\n+            assert r is not None\n+            assert g is not None\n+            assert b is not None\n+            assert a is not None\n+            return QColor(r, g, b, a)\n \n     out = QColor()\n     if colorspace == QColor.Spec.Rgb:\n         r1, g1, b1, a1 = start.getRgb()\n         r2, g2, b2, a2 = end.getRgb()\n+        assert r1 is not None\n+        assert g1 is not None\n+        assert b1 is not None\n+        assert a1 is not None\n+        assert r2 is not None\n+        assert g2 is not None\n+        assert b2 is not None\n+        assert a2 is not None\n         components = _get_color_percentage(r1, g1, b1, a1, r2, g2, b2, a2, percent)\n         out.setRgb(*components)\n     elif colorspace == QColor.Spec.Hsv:\n         h1, s1, v1, a1 = start.getHsv()\n         h2, s2, v2, a2 = end.getHsv()\n+        assert h1 is not None\n+        assert s1 is not None\n+        assert v1 is not None\n+        assert a1 is not None\n+        assert h2 is not None\n+        assert s2 is not None\n+        assert v2 is not None\n+        assert a2 is not None\n         components = _get_color_percentage(h1, s1, v1, a1, h2, s2, v2, a2, percent)\n         out.setHsv(*components)\n     elif colorspace == QColor.Spec.Hsl:\n         h1, s1, l1, a1 = start.getHsl()\n         h2, s2, l2, a2 = end.getHsl()\n+        assert h1 is not None\n+        assert s1 is not None\n+        assert l1 is not None\n+        assert a1 is not None\n+        assert h2 is not None\n+        assert s2 is not None\n+        assert l2 is not None\n+        assert a2 is not None\n         components = _get_color_percentage(h1, s1, l1, a1, h2, s2, l2, a2, percent)\n         out.setHsl(*components)\n     else:\n@@ -577,8 +663,8 @@ class LibraryPath(enum.Enum):\n \n \n def library_path(which: LibraryPath) -&gt; pathlib.Path:\n-    if hasattr(QLibraryInfo, \"path\"):\n-        # Qt 6\n+    \"\"\"Wrapper around QLibraryInfo.path / .location.\"\"\"\n+    if machinery.IS_QT6:\n         val = getattr(QLibraryInfo.LibraryPath, which.value)\n         ret = QLibraryInfo.path(val)\n     else:\n@@ -587,3 +673,77 @@ def library_path(which: LibraryPath) -&gt; pathlib.Path:\n         ret = QLibraryInfo.location(val)\n     assert ret\n     return pathlib.Path(ret)\n+\n+\n+def extract_enum_val(val: Union[sip.simplewrapper, int, enum.Enum]) -&gt; int:\n+    \"\"\"Extract an int value from a Qt enum value.\n+\n+    For Qt 5, enum values are basically Python integers.\n+    For Qt 6, they are usually enum.Enum instances, with the value set to the\n+    integer.\n+    \"\"\"\n+    if isinstance(val, enum.Enum):\n+        return val.value\n+    elif isinstance(val, sip.simplewrapper):\n+        return int(val)  # type: ignore[call-overload]\n+    return val\n+\n+\n+def qobj_repr(obj: Optional[QObject]) -&gt; str:\n+    \"\"\"Show nicer debug information for a QObject.\"\"\"\n+    py_repr = repr(obj)\n+    if obj is None:\n+        return py_repr\n+\n+    try:\n+        object_name = obj.objectName()\n+        meta_object = obj.metaObject()\n+    except AttributeError:\n+        # Technically not possible if obj is a QObject, but crashing when trying to get\n+        # some debug info isn't helpful.\n+        return py_repr\n+\n+    class_name = \"\" if meta_object is None else meta_object.className()\n+\n+    if py_repr.startswith(\"&lt;\") and py_repr.endswith(\"&gt;\"):\n+        # With a repr such as , we want to end up with:\n+        # \n+        # But if we have RichRepr() as existing repr, we want:\n+        # \n+        py_repr = py_repr[1:-1]\n+\n+    parts = [py_repr]\n+    if object_name:\n+        parts.append(f\"objectName={object_name!r}\")\n+    if class_name and f\".{class_name} object at 0x\" not in py_repr:\n+        parts.append(f\"className={class_name!r}\")\n+\n+    return f\"&lt;{', '.join(parts)}&gt;\"\n+\n+\n+_T = TypeVar(\"_T\")\n+\n+\n+if machinery.IS_QT5:\n+    # On Qt 5, add/remove Optional where type annotations don't have it.\n+    # Also we have a special QT_NONE, which (being Any) we can pass to functions\n+    # where PyQt type hints claim that it's not allowed.\n+\n+    def remove_optional(obj: Optional[_T]) -&gt; _T:\n+        return cast(_T, obj)\n+\n+    def add_optional(obj: _T) -&gt; Optional[_T]:\n+        return cast(Optional[_T], obj)\n+\n+    QT_NONE: Any = None\n+else:\n+    # On Qt 6, all those things are handled correctly by type annotations, so we\n+    # have a no-op below.\n+\n+    def remove_optional(obj: Optional[_T]) -&gt; Optional[_T]:\n+        return obj\n+\n+    def add_optional(obj: Optional[_T]) -&gt; Optional[_T]:\n+        return obj\n+\n+    QT_NONE: None = None\ndiff --git a/qutebrowser/utils/resources.py b/qutebrowser/utils/resources.py\nindex 796d8457b..a97a2e994 100644\n--- a/qutebrowser/utils/resources.py\n+++ b/qutebrowser/utils/resources.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Resources related utilities.\"\"\"\n \n@@ -24,10 +9,10 @@ import sys\n import contextlib\n import posixpath\n import pathlib\n-from typing import Iterator, Iterable, Union\n+from typing import Iterator, Iterable, Union, Dict\n \n \n-# We cannot use the stdlib version on 3.7-3.8 because we need the files() API.\n+# We cannot use the stdlib version on 3.8 because we need the files() API.\n if sys.version_info &gt;= (3, 11):  # pragma: no cover\n     # https://github.com/python/cpython/issues/90276\n     import importlib.resources as importlib_resources\n@@ -40,7 +25,8 @@ else:  # pragma: no cover\n     from importlib_resources.abc import Traversable\n \n import qutebrowser\n-_cache = {}\n+_cache: Dict[str, str] = {}\n+_bin_cache: Dict[str, bytes] = {}\n \n \n _ResourceType = Union[Traversable, pathlib.Path]\n@@ -51,11 +37,6 @@ def _path(filename: str) -&gt; _ResourceType:\n     assert not posixpath.isabs(filename), filename\n     assert os.path.pardir not in filename.split(posixpath.sep), filename\n \n-    if hasattr(sys, 'frozen'):\n-        # For PyInstaller, where we can't store resource files in a qutebrowser/ folder\n-        # because the executable is already named \"qutebrowser\" (at least on macOS).\n-        return pathlib.Path(sys.executable).parent / filename\n-\n     return importlib_resources.files(qutebrowser) / filename\n \n @contextlib.contextmanager\n@@ -108,6 +89,10 @@ def preload() -&gt; None:\n         for name in _glob(resource_path, subdir, ext):\n             _cache[name] = read_file(name)\n \n+    for name in _glob(resource_path, 'img', '.png'):\n+        # e.g. broken_qutebrowser_logo.png\n+        _bin_cache[name] = read_file_binary(name)\n+\n \n def read_file(filename: str) -&gt; str:\n     \"\"\"Get the contents of a file contained with qutebrowser.\n@@ -135,6 +120,9 @@ def read_file_binary(filename: str) -&gt; bytes:\n     Return:\n         The file contents as a bytes object.\n     \"\"\"\n+    if filename in _bin_cache:\n+        return _bin_cache[filename]\n+\n     path = _path(filename)\n     with _keyerror_workaround():\n         return path.read_bytes()\ndiff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py\nindex e63b43548..026376dc2 100644\n--- a/qutebrowser/utils/standarddir.py\n+++ b/qutebrowser/utils/standarddir.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utilities to get and initialize data/config paths.\"\"\"\n \n@@ -25,15 +10,15 @@ import sys\n import contextlib\n import enum\n import argparse\n-from typing import Iterator, Optional\n+from typing import Iterator, Optional, Dict\n \n from qutebrowser.qt.core import QStandardPaths\n from qutebrowser.qt.widgets import QApplication\n \n-from qutebrowser.utils import log, debug, utils, version\n+from qutebrowser.utils import log, debug, utils, version, qtutils\n \n # The cached locations\n-_locations = {}\n+_locations: Dict[\"_Location\", str] = {}\n \n \n class _Location(enum.Enum):\n@@ -67,7 +52,7 @@ def _unset_organization() -&gt; Iterator[None]:\n     qapp = QApplication.instance()\n     if qapp is not None:\n         orgname = qapp.organizationName()\n-        qapp.setOrganizationName(None)  # type: ignore[arg-type]\n+        qapp.setOrganizationName(qtutils.QT_NONE)\n     try:\n         yield\n     finally:\n@@ -330,8 +315,10 @@ def _create(path: str) -&gt; None:\n         for k, v in os.environ.items():\n             if k == 'HOME' or k.startswith('XDG_'):\n                 log.init.debug(f\"{k} = {v}\")\n-        raise Exception(\"Trying to create directory inside /home during \"\n-                        \"tests, this should not happen.\")\n+        raise AssertionError(\n+            \"Trying to create directory inside /home during \"\n+            \"tests, this should not happen.\"\n+        )\n     os.makedirs(path, 0o700, exist_ok=True)\n \n \ndiff --git a/qutebrowser/utils/urlmatch.py b/qutebrowser/utils/urlmatch.py\nindex 81127d986..620e4d143 100644\n--- a/qutebrowser/utils/urlmatch.py\n+++ b/qutebrowser/utils/urlmatch.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"A Chromium-like URL matching pattern.\n \ndiff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py\nindex 6fa70dfc9..7b613c0a2 100644\n--- a/qutebrowser/utils/urlutils.py\n+++ b/qutebrowser/utils/urlutils.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utils regarding URL handling.\"\"\"\n \n@@ -26,8 +11,9 @@ import ipaddress\n import posixpath\n import urllib.parse\n import mimetypes\n-from typing import Optional, Tuple, Union, Iterable\n+from typing import Optional, Tuple, Union, Iterable, cast\n \n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import QUrl\n from qutebrowser.qt.network import QHostInfo, QHostAddress, QNetworkProxy\n \n@@ -41,6 +27,58 @@ from qutebrowser.browser.network import pac\n # https://github.com/qutebrowser/qutebrowser/issues/108\n \n \n+if machinery.IS_QT6:\n+    UrlFlagsType = Union[QUrl.UrlFormattingOption, QUrl.ComponentFormattingOption]\n+\n+    class FormatOption:\n+        \"\"\"Simple wrapper around Qt enums to fix typing problems on Qt 5.\"\"\"\n+\n+        ENCODED = QUrl.ComponentFormattingOption.FullyEncoded\n+        ENCODE_UNICODE = QUrl.ComponentFormattingOption.EncodeUnicode\n+        DECODE_RESERVED = QUrl.ComponentFormattingOption.DecodeReserved\n+\n+        REMOVE_SCHEME = QUrl.UrlFormattingOption.RemoveScheme\n+        REMOVE_PASSWORD = QUrl.UrlFormattingOption.RemovePassword\n+        REMOVE_QUERY = QUrl.UrlFormattingOption.RemoveQuery\n+else:\n+    UrlFlagsType = Union[\n+        QUrl.FormattingOptions,\n+        QUrl.UrlFormattingOption,\n+        QUrl.ComponentFormattingOption,\n+        QUrl.ComponentFormattingOptions,\n+    ]\n+\n+    class _QtFormattingOptions(QUrl.FormattingOptions):\n+        \"\"\"WORKAROUND for invalid stubs.\n+\n+        Teach mypy that | works for QUrl.FormattingOptions.\n+        \"\"\"\n+\n+        def __or__(self, f: UrlFlagsType) -&gt; '_QtFormattingOptions':\n+            return super() | f  # type: ignore[operator,return-value]\n+\n+    class FormatOption:\n+        \"\"\"WORKAROUND for invalid stubs.\n+\n+        Pretend that all ComponentFormattingOption values are also valid\n+        QUrl.FormattingOptions values, i.e. can be passed to QUrl.toString().\n+        \"\"\"\n+\n+        ENCODED = cast(\n+            _QtFormattingOptions, QUrl.ComponentFormattingOption.FullyEncoded)\n+        ENCODE_UNICODE = cast(\n+            _QtFormattingOptions, QUrl.ComponentFormattingOption.EncodeUnicode)\n+        DECODE_RESERVED = cast(\n+            _QtFormattingOptions, QUrl.ComponentFormattingOption.DecodeReserved)\n+\n+        REMOVE_SCHEME = cast(\n+            _QtFormattingOptions, QUrl.UrlFormattingOption.RemoveScheme)\n+        REMOVE_PASSWORD = cast(\n+            _QtFormattingOptions, QUrl.UrlFormattingOption.RemovePassword)\n+        REMOVE_QUERY = cast(\n+            _QtFormattingOptions, QUrl.UrlFormattingOption.RemoveQuery)\n+\n+\n # URL schemes supported by QtWebEngine\n WEBENGINE_SCHEMES = [\n     'about',\n@@ -131,9 +169,9 @@ def _get_search_url(txt: str) -&gt; QUrl:\n         url = QUrl.fromUserInput(evaluated)\n     else:\n         url = QUrl.fromUserInput(config.val.url.searchengines[engine])\n-        url.setPath(None)  # type: ignore[arg-type]\n-        url.setFragment(None)  # type: ignore[arg-type]\n-        url.setQuery(None)  # type: ignore[call-overload]\n+        url.setPath(qtutils.QT_NONE)\n+        url.setFragment(qtutils.QT_NONE)\n+        url.setQuery(qtutils.QT_NONE)\n     qtutils.ensure_valid(url)\n     return url\n \n@@ -188,7 +226,7 @@ def _is_url_dns(urlstr: str) -&gt; bool:\n         return False\n     log.url.debug(\"Doing DNS request for {}\".format(host))\n     info = QHostInfo.fromName(host)\n-    return not info.error()\n+    return info.error() == QHostInfo.HostInfoError.NoError\n \n \n def fuzzy_url(urlstr: str,\n@@ -493,8 +531,22 @@ def same_domain(url1: QUrl, url2: QUrl) -&gt; bool:\n     if url1.port() != url2.port():\n         return False\n \n-    suffix1 = url1.topLevelDomain()\n-    suffix2 = url2.topLevelDomain()\n+    # QUrl.topLevelDomain() got removed in Qt 6:\n+    # https://bugreports.qt.io/browse/QTBUG-80308\n+    #\n+    # However, we should never land here if we are on Qt 6:\n+    #\n+    # On QtWebEngine, we don't have a QNetworkAccessManager attached to a tab\n+    # (all tab-specific downloads happen via the QtWebEngine network stack).\n+    # Thus, ensure_valid(url2) above will raise InvalidUrlError, which is\n+    # handled in NetworkManager.\n+    #\n+    # There are no other callers of same_domain, and url2 will only be ever valid when\n+    # we use a NetworkManager from QtWebKit. However, QtWebKit is Qt 5 only.\n+    assert machinery.IS_QT5, machinery.INFO\n+\n+    suffix1 = url1.topLevelDomain()  # type: ignore[attr-defined,unused-ignore]\n+    suffix2 = url2.topLevelDomain()  # type: ignore[attr-defined,unused-ignore]\n     if not suffix1:\n         return url1.host() == url2.host()\n \n@@ -522,7 +574,7 @@ def file_url(path: str) -&gt; str:\n         path: The absolute path to the local file\n     \"\"\"\n     url = QUrl.fromLocalFile(path)\n-    return url.toString(QUrl.ComponentFormattingOption.FullyEncoded)  # type: ignore[arg-type]\n+    return url.toString(FormatOption.ENCODED)\n \n \n def data_url(mimetype: str, data: bytes) -&gt; QUrl:\n@@ -613,7 +665,7 @@ def parse_javascript_url(url: QUrl) -&gt; str:\n         raise Error(\"URL contains unexpected components: {}\"\n                     .format(url.authority()))\n \n-    urlstr = url.toString(QUrl.ComponentFormattingOption.FullyEncoded)  # type: ignore[arg-type]\n+    urlstr = url.toString(FormatOption.ENCODED)\n     urlstr = urllib.parse.unquote(urlstr)\n \n     code = urlstr[len('javascript:'):]\ndiff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py\nindex 6bd9ce448..d61d4aba7 100644\n--- a/qutebrowser/utils/usertypes.py\n+++ b/qutebrowser/utils/usertypes.py\n@@ -1,28 +1,15 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Custom useful data types.\"\"\"\n \n import html\n import operator\n import enum\n+import time\n import dataclasses\n+import logging\n from typing import Optional, Sequence, TypeVar, Union\n \n from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, QTimer\n@@ -458,6 +445,8 @@ class Timer(QTimer):\n \n     def __init__(self, parent: QObject = None, name: str = None) -&gt; None:\n         super().__init__(parent)\n+        self._start_time: Optional[float] = None\n+        self.timeout.connect(self._validity_check_handler)\n         if name is None:\n             self._name = \"unnamed\"\n         else:\n@@ -467,6 +456,39 @@ class Timer(QTimer):\n     def __repr__(self) -&gt; str:\n         return utils.get_repr(self, name=self._name)\n \n+    @pyqtSlot()\n+    def _validity_check_handler(self) -&gt; None:\n+        if not self.check_timeout_validity() and self._start_time is not None:\n+            elapsed = time.monotonic() - self._start_time\n+            level = logging.WARNING\n+            if utils.is_windows and self._name == \"ipc-timeout\":\n+                level = logging.DEBUG\n+            log.misc.log(\n+                level,\n+                (\n+                    f\"Timer {self._name} (id {self.timerId()}) triggered too early: \"\n+                    f\"interval {self.interval()} but only {elapsed:.3f}s passed\"\n+                )\n+            )\n+\n+    def check_timeout_validity(self) -&gt; bool:\n+        \"\"\"Check to see if the timeout signal was fired at the expected time.\n+\n+        WORKAROUND for https://bugreports.qt.io/browse/QTBUG-124496\n+        \"\"\"\n+        if self._start_time is None:\n+            # manual emission?\n+            return True\n+\n+        elapsed = time.monotonic() - self._start_time\n+        # Checking for half the interval is pretty arbitrary. In the bug case\n+        # the timer typically fires immediately since the expiry event is\n+        # already pending when it is created.\n+        if elapsed &lt; self.interval() / 1000 / 2:\n+            return False\n+\n+        return True\n+\n     def setInterval(self, msec: int) -&gt; None:\n         \"\"\"Extend setInterval to check for overflows.\"\"\"\n         qtutils.check_overflow(msec, 'int')\n@@ -474,6 +496,7 @@ class Timer(QTimer):\n \n     def start(self, msec: int = None) -&gt; None:\n         \"\"\"Extend start to check for overflows.\"\"\"\n+        self._start_time = time.monotonic()\n         if msec is not None:\n             qtutils.check_overflow(msec, 'int')\n             super().start(msec)\n@@ -481,10 +504,18 @@ class Timer(QTimer):\n             super().start()\n \n \n+class UndeferrableError(Exception):\n+\n+    \"\"\"An AbstractCertificateErrorWrapper isn't deferrable.\"\"\"\n+\n+\n class AbstractCertificateErrorWrapper:\n \n     \"\"\"A wrapper over an SSL/certificate error.\"\"\"\n \n+    def __init__(self) -&gt; None:\n+        self._certificate_accepted: Optional[bool] = None\n+\n     def __str__(self) -&gt; str:\n         raise NotImplementedError\n \n@@ -497,6 +528,23 @@ class AbstractCertificateErrorWrapper:\n     def html(self) -&gt; str:\n         return f'\n{html.escape(str(self))}'\n \n+    def accept_certificate(self) -&gt; None:\n+        self._certificate_accepted = True\n+\n+    def reject_certificate(self) -&gt; None:\n+        self._certificate_accepted = False\n+\n+    def defer(self) -&gt; None:\n+        raise NotImplementedError\n+\n+    def certificate_was_accepted(self) -&gt; bool:\n+        \"\"\"Check whether the certificate was accepted by the user.\"\"\"\n+        if not self.is_overridable():\n+            return False\n+        if self._certificate_accepted is None:\n+            raise ValueError(\"No decision taken yet\")\n+        return self._certificate_accepted\n+\n \n @dataclasses.dataclass\n class NavigationRequest:\n@@ -521,7 +569,7 @@ class NavigationRequest:\n         #: Navigation initiated by a history action.\n         back_forward = 5\n         #: Navigation initiated by refreshing the page.\n-        reloaded = 6\n+        reload = 6\n         #: Navigation triggered automatically by page content or remote server\n         #: (QtWebEngine &gt;= 5.14 only)\n         redirect = 7\ndiff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py\nindex ddd82cb76..13ccf5ca2 100644\n--- a/qutebrowser/utils/utils.py\n+++ b/qutebrowser/utils/utils.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Other utilities which don't fit anywhere else.\"\"\"\n \n@@ -32,18 +17,11 @@ import traceback\n import functools\n import contextlib\n import shlex\n+import sysconfig\n import mimetypes\n from typing import (Any, Callable, IO, Iterator,\n                     Optional, Sequence, Tuple, List, Type, Union,\n-                    TypeVar, TYPE_CHECKING)\n-try:\n-    # Protocol was added in Python 3.8\n-    from typing import Protocol\n-except ImportError:  # pragma: no cover\n-    if not TYPE_CHECKING:\n-        class Protocol:\n-\n-            \"\"\"Empty stub at runtime.\"\"\"\n+                    TypeVar, Protocol)\n \n from qutebrowser.qt.core import QUrl, QVersionNumber, QRect, QPoint\n from qutebrowser.qt.gui import QClipboard, QDesktopServices\n@@ -55,13 +33,13 @@ try:\n                       CSafeDumper as YamlDumper)\n     YAML_C_EXT = True\n except ImportError:  # pragma: no cover\n-    from yaml import (SafeLoader as YamlLoader,  # type: ignore[misc]\n+    from yaml import (SafeLoader as YamlLoader,  # type: ignore[assignment]\n                       SafeDumper as YamlDumper)\n     YAML_C_EXT = False\n \n from qutebrowser.utils import log\n \n-fake_clipboard = None\n+fake_clipboard: Optional[str] = None\n log_clipboard = False\n \n is_mac = sys.platform.startswith('darwin')\n@@ -138,17 +116,20 @@ class VersionNumber:\n             return NotImplemented\n         return self._ver != other._ver\n \n+    # FIXME:mypy type ignores below needed for PyQt5-stubs:\n+    # Unsupported left operand type for ... (\"QVersionNumber\")\n+\n     def __ge__(self, other: 'VersionNumber') -&gt; bool:\n-        return self._ver &gt;= other._ver  # type: ignore[operator]\n+        return self._ver &gt;= other._ver  # type: ignore[operator,unused-ignore]\n \n     def __gt__(self, other: 'VersionNumber') -&gt; bool:\n-        return self._ver &gt; other._ver  # type: ignore[operator]\n+        return self._ver &gt; other._ver  # type: ignore[operator,unused-ignore]\n \n     def __le__(self, other: 'VersionNumber') -&gt; bool:\n-        return self._ver &lt;= other._ver  # type: ignore[operator]\n+        return self._ver &lt;= other._ver  # type: ignore[operator,unused-ignore]\n \n     def __lt__(self, other: 'VersionNumber') -&gt; bool:\n-        return self._ver &lt; other._ver  # type: ignore[operator]\n+        return self._ver &lt; other._ver  # type: ignore[operator,unused-ignore]\n \n \n class Unreachable(Exception):\n@@ -270,7 +251,7 @@ class FakeIOStream(io.TextIOBase):\n \n     def __init__(self, write_func: Callable[[str], int]) -&gt; None:\n         super().__init__()\n-        self.write = write_func  # type: ignore[assignment]\n+        self.write = write_func  # type: ignore[method-assign]\n \n \n @contextlib.contextmanager\n@@ -361,7 +342,7 @@ class prevent_exceptions:  # noqa: N801,N806 pylint: disable=invalid-name\n             \"\"\"Call the original function.\"\"\"\n             try:\n                 return func(*args, **kwargs)\n-            except BaseException:\n+            except BaseException:  # noqa: B036\n                 log.misc.exception(\"Error in {}\".format(qualname(func)))\n                 return retval\n \n@@ -526,6 +507,13 @@ def sanitize_filename(name: str,\n     return name\n \n \n+def _clipboard() -&gt; QClipboard:\n+    \"\"\"Get the QClipboard and make sure it's not None.\"\"\"\n+    clipboard = QApplication.clipboard()\n+    assert clipboard is not None\n+    return clipboard\n+\n+\n def set_clipboard(data: str, selection: bool = False) -&gt; None:\n     \"\"\"Set the clipboard to some given data.\"\"\"\n     global fake_clipboard\n@@ -537,7 +525,7 @@ def set_clipboard(data: str, selection: bool = False) -&gt; None:\n         fake_clipboard = data\n     else:\n         mode = QClipboard.Mode.Selection if selection else QClipboard.Mode.Clipboard\n-        QApplication.clipboard().setText(data, mode=mode)\n+        _clipboard().setText(data, mode=mode)\n \n \n def get_clipboard(selection: bool = False, fallback: bool = False) -&gt; str:\n@@ -563,7 +551,7 @@ def get_clipboard(selection: bool = False, fallback: bool = False) -&gt; str:\n         fake_clipboard = None\n     else:\n         mode = QClipboard.Mode.Selection if selection else QClipboard.Mode.Clipboard\n-        data = QApplication.clipboard().text(mode=mode)\n+        data = _clipboard().text(mode=mode)\n \n     target = \"Primary selection\" if selection else \"Clipboard\"\n     if not data.strip():\n@@ -575,7 +563,7 @@ def get_clipboard(selection: bool = False, fallback: bool = False) -&gt; str:\n \n def supports_selection() -&gt; bool:\n     \"\"\"Check if the OS supports primary selection.\"\"\"\n-    return QApplication.clipboard().supportsSelection()\n+    return _clipboard().supportsSelection()\n \n \n def open_file(filename: str, cmdline: str = None) -&gt; None:\n@@ -649,7 +637,7 @@ def expand_windows_drive(path: str) -&gt; str:\n         path: The path to expand.\n     \"\"\"\n     # Usually, \"E:\" on Windows refers to the current working directory on drive\n-    # E:\\. The correct way to specifify drive E: is \"E:\\\", but most users\n+    # E:\\. The correct way to specify drive E: is \"E:\\\", but most users\n     # probably don't use the \"multiple working directories\" feature and expect\n     # \"E:\" and \"E:\\\" to be equal.\n     if re.fullmatch(r'[A-Z]:', path, re.IGNORECASE):\n@@ -679,7 +667,10 @@ def yaml_load(f: Union[str, IO[str]]) -&gt; Any:\n     end = datetime.datetime.now()\n \n     delta = (end - start).total_seconds()\n-    deadline = 10 if 'CI' in os.environ else 2\n+    if \"CI\" in os.environ or sysconfig.get_config_var(\"Py_DEBUG\"):\n+        deadline = 10\n+    else:\n+        deadline = 2\n     if delta &gt; deadline:  # pragma: no cover\n         log.misc.warning(\n             \"YAML load took unusually long, please report this at \"\ndiff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py\nindex 8dfef08ff..2bb39fea0 100644\n--- a/qutebrowser/utils/version.py\n+++ b/qutebrowser/utils/version.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Utilities to show various version information.\"\"\"\n \n@@ -26,7 +11,6 @@ import os.path\n import platform\n import subprocess\n import importlib\n-import collections\n import pathlib\n import configparser\n import enum\n@@ -34,11 +18,12 @@ import datetime\n import getpass\n import functools\n import dataclasses\n-from typing import (Mapping, Optional, Sequence, Tuple, ClassVar, Dict, cast,\n+import importlib.metadata\n+from typing import (Mapping, Optional, Sequence, Tuple, ClassVar, Dict, Any,\n                     TYPE_CHECKING)\n \n-\n-from qutebrowser.qt.core import PYQT_VERSION_STR, qVersion\n+from qutebrowser.qt import machinery\n+from qutebrowser.qt.core import PYQT_VERSION_STR\n from qutebrowser.qt.network import QSslSocket\n from qutebrowser.qt.gui import QOpenGLContext, QOffscreenSurface\n from qutebrowser.qt.opengl import QOpenGLVersionProfile\n@@ -51,7 +36,7 @@ except ImportError:  # pragma: no cover\n try:\n     from qutebrowser.qt.webenginecore import PYQT_WEBENGINE_VERSION_STR\n except ImportError:  # pragma: no cover\n-    # Added in PyQt 5.13\n+    # QtWebKit\n     PYQT_WEBENGINE_VERSION_STR = None  # type: ignore[assignment]\n \n \n@@ -93,7 +78,7 @@ class DistributionInfo:\n     pretty: str\n \n \n-pastebin_url = None\n+pastebin_url: Optional[str] = None\n \n \n class Distribution(enum.Enum):\n@@ -394,23 +379,39 @@ class ModuleInfo:\n         return text\n \n \n-MODULE_INFO: Mapping[str, ModuleInfo] = collections.OrderedDict([\n-    # FIXME: Mypy doesn't understand this. See https://github.com/python/mypy/issues/9706\n-    (name, ModuleInfo(name, *args))  # type: ignore[arg-type, misc]\n-    for (name, *args) in\n-    [\n-        ('sip', ['SIP_VERSION_STR']),\n+def _create_module_info() -&gt; Dict[str, ModuleInfo]:\n+    packages = [\n         ('colorama', ['VERSION', '__version__']),\n         ('jinja2', ['__version__']),\n         ('pygments', ['__version__']),\n         ('yaml', ['__version__']),\n         ('adblock', ['__version__'], \"0.3.2\"),\n-        ('PyQt5.QtWebEngineWidgets', []),\n-        ('PyQt5.QtWebEngine', ['PYQT_WEBENGINE_VERSION_STR']),\n-        ('PyQt5.QtWebKitWidgets', []),\n         ('objc', ['__version__']),\n     ]\n-])\n+\n+    if machinery.IS_QT5:\n+        packages += [\n+            ('PyQt5.QtWebEngineWidgets', []),\n+            ('PyQt5.QtWebEngine', ['PYQT_WEBENGINE_VERSION_STR']),\n+            ('PyQt5.QtWebKitWidgets', []),\n+            ('PyQt5.sip', ['SIP_VERSION_STR']),\n+        ]\n+    elif machinery.IS_QT6:\n+        packages += [\n+            ('PyQt6.QtWebEngineCore', ['PYQT_WEBENGINE_VERSION_STR']),\n+            ('PyQt6.sip', ['SIP_VERSION_STR']),\n+        ]\n+    else:\n+        raise utils.Unreachable()\n+\n+    # Mypy doesn't understand this. See https://github.com/python/mypy/issues/9706\n+    return {\n+        name: ModuleInfo(name, *args)  # type: ignore[arg-type, misc]\n+        for (name, *args) in packages\n+    }\n+\n+\n+MODULE_INFO: Mapping[str, ModuleInfo] = _create_module_info()\n \n \n def _module_versions() -&gt; Sequence[str]:\n@@ -479,13 +480,13 @@ def _pdfjs_version() -&gt; str:\n         A string with the version number.\n     \"\"\"\n     try:\n-        pdfjs_file, file_path = pdfjs.get_pdfjs_res_and_path('build/pdf.js')\n+        pdfjs_file, file_path = pdfjs.get_pdfjs_res_and_path(pdfjs.get_pdfjs_js_path())\n     except pdfjs.PDFJSNotFound:\n         return 'no'\n     else:\n         pdfjs_file = pdfjs_file.decode('utf-8')\n         version_re = re.compile(\n-            r\"^ *(PDFJS\\.version|(var|const) pdfjsVersion) = '(?P[^']+)';$\",\n+            r\"\"\"^ *(PDFJS\\.version|(var|const) pdfjsVersion) = ['\"](?P[^'\"]+)['\"];$\"\"\",\n             re.MULTILINE)\n \n         match = version_re.search(pdfjs_file)\n@@ -508,24 +509,20 @@ def _get_pyqt_webengine_qt_version() -&gt; Optional[str]:\n     PyQtWebEngine 5.15.4 renamed it to PyQtWebEngine-Qt5...:\n     https://www.riverbankcomputing.com/pipermail/pyqt/2021-March/043699.html\n \n-    Here, we try to use importlib.metadata or its backport (optional dependency) to\n-    figure out that version number. If PyQtWebEngine is installed via pip, this will\n-    give us an accurate answer.\n+    Here, we try to use importlib.metadata to figure out that version number.\n+    If PyQtWebEngine is installed via pip, this will give us an accurate answer.\n     \"\"\"\n-    try:\n-        import importlib.metadata as importlib_metadata  # type: ignore[import]\n-    except ImportError:\n-        try:\n-            import importlib_metadata  # type: ignore[no-redef]\n-        except ImportError:\n-            log.misc.debug(\"Neither importlib.metadata nor backport available\")\n-            return None\n+    names = (\n+        ['PyQt6-WebEngine-Qt6']\n+        if machinery.IS_QT6 else\n+        ['PyQtWebEngine-Qt5', 'PyQtWebEngine-Qt']\n+    )\n \n-    for suffix in ['Qt5', 'Qt']:\n+    for name in names:\n         try:\n-            return importlib_metadata.version(f'PyQtWebEngine-{suffix}')\n-        except importlib_metadata.PackageNotFoundError:\n-            log.misc.debug(f\"PyQtWebEngine-{suffix} not found\")\n+            return importlib.metadata.version(name)\n+        except importlib.metadata.PackageNotFoundError:\n+            log.misc.debug(f\"{name} not found\")\n \n     return None\n \n@@ -538,49 +535,97 @@ class WebEngineVersions:\n     webengine: utils.VersionNumber\n     chromium: Optional[str]\n     source: str\n+    chromium_security: Optional[str] = None\n     chromium_major: Optional[int] = dataclasses.field(init=False)\n \n-    _CHROMIUM_VERSIONS: ClassVar[Dict[utils.VersionNumber, str]] = {\n+    _BASES: ClassVar[Dict[int, str]] = {\n+        83: '83.0.4103.122',  # ~2020-06-24\n+        87: '87.0.4280.144',  # ~2020-12-02\n+        90: '90.0.4430.228',  # 2021-06-22\n+        94: '94.0.4606.126',  # 2021-11-17\n+        102: '102.0.5005.177',  # ~2022-05-24\n+        # (.220 claimed by code, .181 claimed by CHROMIUM_VERSION)\n+        108: '108.0.5359.220',  # ~2022-12-23\n+        112: '112.0.5615.213',  # ~2023-04-18\n+        118: '118.0.5993.220',  # ~2023-10-24\n+    }\n+\n+    _CHROMIUM_VERSIONS: ClassVar[Dict[utils.VersionNumber, Tuple[str, Optional[str]]]] = {\n+        # ====== UNSUPPORTED =====\n+\n         # Qt 5.12: Chromium 69\n         # (LTS)    69.0.3497.128 (~2018-09-11)\n-        #          5.12.0: Security fixes up to 70.0.3538.102 (~2018-10-24)\n-        #          5.12.1: Security fixes up to 71.0.3578.94  (2018-12-12)\n-        #          5.12.2: Security fixes up to 72.0.3626.121 (2019-03-01)\n-        #          5.12.3: Security fixes up to 73.0.3683.75  (2019-03-12)\n-        #          5.12.4: Security fixes up to 74.0.3729.157 (2019-05-14)\n-        #          5.12.5: Security fixes up to 76.0.3809.87  (2019-07-30)\n-        #          5.12.6: Security fixes up to 77.0.3865.120 (~2019-09-10)\n-        #          5.12.7: Security fixes up to 79.0.3945.130 (2020-01-16)\n-        #          5.12.8: Security fixes up to 80.0.3987.149 (2020-03-18)\n-        #          5.12.9: Security fixes up to 83.0.4103.97  (2020-06-03)\n         #          5.12.10: Security fixes up to 86.0.4240.75 (2020-10-06)\n-        utils.VersionNumber(5, 12): '69.0.3497.128',\n \n         # Qt 5.13: Chromium 73\n         #          73.0.3683.105 (~2019-02-28)\n-        #          5.13.0: Security fixes up to 74.0.3729.157 (2019-05-14)\n-        #          5.13.1: Security fixes up to 76.0.3809.87  (2019-07-30)\n         #          5.13.2: Security fixes up to 77.0.3865.120 (2019-10-10)\n-        utils.VersionNumber(5, 13): '73.0.3683.105',\n \n         # Qt 5.14: Chromium 77\n         #          77.0.3865.129 (~2019-10-10)\n-        #          5.14.0: Security fixes up to 77.0.3865.129 (~2019-09-10)\n-        #          5.14.1: Security fixes up to 79.0.3945.117 (2020-01-07)\n         #          5.14.2: Security fixes up to 80.0.3987.132 (2020-03-03)\n-        utils.VersionNumber(5, 14): '77.0.3865.129',\n \n         # Qt 5.15: Chromium 80\n         #          80.0.3987.163 (2020-04-02)\n         #          5.15.0: Security fixes up to 81.0.4044.138 (2020-05-05)\n         #          5.15.1: Security fixes up to 85.0.4183.83  (2020-08-25)\n-        #          5.15.2: Updated to 83.0.4103.122           (~2020-06-24)\n-        #                  Security fixes up to 86.0.4240.183 (2020-11-02)\n-        #          5.15.3: Updated to 87.0.4280.144           (~2020-12-02)\n-        #                  Security fixes up to 88.0.4324.150 (2021-02-04)\n-        utils.VersionNumber(5, 15): '80.0.3987.163',\n-        utils.VersionNumber(5, 15, 2): '83.0.4103.122',\n-        utils.VersionNumber(5, 15, 3): '87.0.4280.144',\n+\n+        # ====== SUPPORTED =====\n+        #                               base         security\n+        ## Qt 5.15\n+        utils.VersionNumber(5, 15, 2): (_BASES[83], '86.0.4240.183'),  # 2020-11-02\n+        utils.VersionNumber(5, 15): (_BASES[87], None),  # &gt;= 5.15.3\n+        utils.VersionNumber(5, 15, 3): (_BASES[87], '88.0.4324.150'),  # 2021-02-04\n+        # 5.15.4 to 5.15.6: unknown security fixes\n+        utils.VersionNumber(5, 15, 7): (_BASES[87], '94.0.4606.61'),  # 2021-09-24\n+        utils.VersionNumber(5, 15, 8): (_BASES[87], '96.0.4664.110'),  # 2021-12-13\n+        utils.VersionNumber(5, 15, 9): (_BASES[87], '98.0.4758.102'),  # 2022-02-14\n+        utils.VersionNumber(5, 15, 10): (_BASES[87], '98.0.4758.102'),  # (?) 2022-02-14\n+        utils.VersionNumber(5, 15, 11): (_BASES[87], '98.0.4758.102'),  # (?) 2022-02-14\n+        utils.VersionNumber(5, 15, 12): (_BASES[87], '98.0.4758.102'),  # (?) 2022-02-14\n+        utils.VersionNumber(5, 15, 13): (_BASES[87], '108.0.5359.124'),  # 2022-12-13\n+        utils.VersionNumber(5, 15, 14): (_BASES[87], '113.0.5672.64'),  # 2023-05-02\n+        # 5.15.15: unknown security fixes\n+        utils.VersionNumber(5, 15, 16): (_BASES[87], '119.0.6045.123'),  # 2023-11-07\n+        utils.VersionNumber(5, 15, 17): (_BASES[87], '123.0.6312.58'),  # 2024-03-19\n+\n+\n+        ## Qt 6.2\n+        utils.VersionNumber(6, 2): (_BASES[90], '93.0.4577.63'),  # 2021-08-31\n+        utils.VersionNumber(6, 2, 1): (_BASES[90], '94.0.4606.61'),  # 2021-09-24\n+        utils.VersionNumber(6, 2, 2): (_BASES[90], '96.0.4664.45'),  # 2021-11-15\n+        utils.VersionNumber(6, 2, 3): (_BASES[90], '96.0.4664.45'),  # 2021-11-15\n+        utils.VersionNumber(6, 2, 4): (_BASES[90], '98.0.4758.102'),  # 2022-02-14\n+        # 6.2.5 / 6.2.6: unknown security fixes\n+        utils.VersionNumber(6, 2, 7): (_BASES[90], '107.0.5304.110'),  # 2022-11-08\n+        utils.VersionNumber(6, 2, 8): (_BASES[90], '111.0.5563.110'),  # 2023-03-21\n+\n+        ## Qt 6.3\n+        utils.VersionNumber(6, 3): (_BASES[94], '99.0.4844.84'),  # 2022-03-25\n+        utils.VersionNumber(6, 3, 1): (_BASES[94], '101.0.4951.64'),  # 2022-05-10\n+        utils.VersionNumber(6, 3, 2): (_BASES[94], '104.0.5112.81'),  # 2022-08-01\n+\n+        ## Qt 6.4\n+        utils.VersionNumber(6, 4): (_BASES[102], '104.0.5112.102'),  # 2022-08-16\n+        utils.VersionNumber(6, 4, 1): (_BASES[102], '107.0.5304.88'),  # 2022-10-27\n+        utils.VersionNumber(6, 4, 2): (_BASES[102], '108.0.5359.94'),  # 2022-12-02\n+        utils.VersionNumber(6, 4, 3): (_BASES[102], '110.0.5481.78'),  # 2023-02-07\n+\n+        ## Qt 6.5\n+        utils.VersionNumber(6, 5): (_BASES[108], '110.0.5481.104'),  # 2023-02-16\n+        utils.VersionNumber(6, 5, 1): (_BASES[108], '112.0.5615.138'),  # 2023-04-18\n+        utils.VersionNumber(6, 5, 2): (_BASES[108], '114.0.5735.133'),  # 2023-06-13\n+        utils.VersionNumber(6, 5, 3): (_BASES[108], '117.0.5938.63'),  # 2023-09-12\n+\n+        ## Qt 6.6\n+        utils.VersionNumber(6, 6): (_BASES[112], '117.0.5938.63'),  # 2023-09-12\n+        utils.VersionNumber(6, 6, 1): (_BASES[112], '119.0.6045.123'),  # 2023-11-07\n+        utils.VersionNumber(6, 6, 2): (_BASES[112], '121.0.6167.160'),  # 2024-02-06\n+        utils.VersionNumber(6, 6, 3): (_BASES[112], '122.0.6261.128'),  # 2024-03-12\n+\n+        ## Qt 6.7\n+        utils.VersionNumber(6, 7): (_BASES[118], '122.0.6261.128'),  # 2024-03-12\n+        utils.VersionNumber(6, 7, 1): (_BASES[118], '124.0.6367.202'),  # ~2024-05-09\n     }\n \n     def __post_init__(self) -&gt; None:\n@@ -591,25 +636,37 @@ class WebEngineVersions:\n             self.chromium_major = int(self.chromium.split('.')[0])\n \n     def __str__(self) -&gt; str:\n-        s = f'QtWebEngine {self.webengine}'\n+        lines = [f'QtWebEngine {self.webengine}']\n         if self.chromium is not None:\n-            s += f', based on Chromium {self.chromium}'\n-        if self.source != 'UA':\n-            s += f' (from {self.source})'\n-        return s\n+            lines.append(f'  based on Chromium {self.chromium}')\n+        if self.chromium_security is not None:\n+            lines.append(f'  with security patches up to {self.chromium_security} (plus any distribution patches)')\n+        lines.append(f'  (source: {self.source})')\n+        return \"\\n\".join(lines)\n \n     @classmethod\n     def from_ua(cls, ua: 'websettings.UserAgent') -&gt; 'WebEngineVersions':\n         \"\"\"Get the versions parsed from a user agent.\n \n-        This is the most reliable and \"default\" way to get this information (at least\n-        until QtWebEngine adds an API for it). However, it needs a fully initialized\n-        QtWebEngine, and we sometimes need this information before that is available.\n+        This is the most reliable and \"default\" way to get this information for\n+        older Qt versions that don't provide an API for it. However, it needs a\n+        fully initialized QtWebEngine, and we sometimes need this information\n+        before that is available.\n         \"\"\"\n         assert ua.qt_version is not None, ua\n+        webengine = utils.VersionNumber.parse(ua.qt_version)\n+        chromium_inferred, chromium_security = cls._infer_chromium_version(webengine)\n+        if ua.upstream_browser_version != chromium_inferred:  # pragma: no cover\n+            # should never happen, but let's play it safe\n+            log.misc.debug(\n+                f\"Chromium version mismatch: {ua.upstream_browser_version} (UA) != \"\n+                f\"{chromium_inferred} (inferred)\")\n+            chromium_security = None\n+\n         return cls(\n-            webengine=utils.VersionNumber.parse(ua.qt_version),\n+            webengine=webengine,\n             chromium=ua.upstream_browser_version,\n+            chromium_security=chromium_security,\n             source='UA',\n         )\n \n@@ -624,9 +681,19 @@ class WebEngineVersions:\n         sometimes mix and match Qt/QtWebEngine versions, so this is a more reliable\n         (though hackish) way to get a more accurate result.\n         \"\"\"\n+        webengine = utils.VersionNumber.parse(versions.webengine)\n+        chromium_inferred, chromium_security = cls._infer_chromium_version(webengine)\n+        if versions.chromium != chromium_inferred:  # pragma: no cover\n+            # should never happen, but let's play it safe\n+            log.misc.debug(\n+                f\"Chromium version mismatch: {versions.chromium} (ELF) != \"\n+                f\"{chromium_inferred} (inferred)\")\n+            chromium_security = None\n+\n         return cls(\n-            webengine=utils.VersionNumber.parse(versions.webengine),\n+            webengine=webengine,\n             chromium=versions.chromium,\n+            chromium_security=chromium_security,\n             source='ELF',\n         )\n \n@@ -634,25 +701,56 @@ class WebEngineVersions:\n     def _infer_chromium_version(\n             cls,\n             pyqt_webengine_version: utils.VersionNumber,\n-    ) -&gt; Optional[str]:\n-        \"\"\"Infer the Chromium version based on the PyQtWebEngine version.\"\"\"\n-        chromium_version = cls._CHROMIUM_VERSIONS.get(pyqt_webengine_version)\n+    ) -&gt; Tuple[Optional[str], Optional[str]]:\n+        \"\"\"Infer the Chromium version based on the PyQtWebEngine version.\n+\n+        Returns:\n+            A tuple of the Chromium version and the security patch version.\n+        \"\"\"\n+        chromium_version, security_version = cls._CHROMIUM_VERSIONS.get(\n+            pyqt_webengine_version, (None, None))\n         if chromium_version is not None:\n-            return chromium_version\n+            return chromium_version, security_version\n \n         # 5.15 patch versions change their QtWebEngine version, but no changes are\n-        # expected after 5.15.3.\n-        v5_15_3 = utils.VersionNumber(5, 15, 3)\n-        if v5_15_3 &lt;= pyqt_webengine_version &lt; utils.VersionNumber(6):\n-            minor_version = v5_15_3\n-        else:\n-            # e.g. 5.14.2 -&gt; 5.14\n-            minor_version = pyqt_webengine_version.strip_patch()\n+        # expected after 5.15.3 and 5.15.[01] are unsupported.\n+        assert pyqt_webengine_version != utils.VersionNumber(5, 15, 2)\n \n-        return cls._CHROMIUM_VERSIONS.get(minor_version)\n+        # e.g. 5.15.4 -&gt; 5.15\n+        # we ignore the security version as that one will have changed from .0\n+        # and is thus unknown.\n+        minor_version = pyqt_webengine_version.strip_patch()\n+        chromium_ver, _security_ver = cls._CHROMIUM_VERSIONS.get(\n+            minor_version, (None, None))\n+\n+        return chromium_ver, None\n+\n+    @classmethod\n+    def from_api(\n+        cls,\n+        qtwe_version: str,\n+        chromium_version: Optional[str],\n+        chromium_security: Optional[str] = None,\n+    ) -&gt; 'WebEngineVersions':\n+        \"\"\"Get the versions based on the exact versions.\n+\n+        This is called if we have proper APIs to get the versions easily\n+        (Qt 6.2 with PyQt 6.3.1+).\n+        \"\"\"\n+        parsed = utils.VersionNumber.parse(qtwe_version)\n+        return cls(\n+            webengine=parsed,\n+            chromium=chromium_version,\n+            chromium_security=chromium_security,\n+            source='api',\n+        )\n \n     @classmethod\n-    def from_importlib(cls, pyqt_webengine_qt_version: str) -&gt; 'WebEngineVersions':\n+    def from_webengine(\n+        cls,\n+        pyqt_webengine_qt_version: str,\n+        source: str,\n+    ) -&gt; 'WebEngineVersions':\n         \"\"\"Get the versions based on the PyQtWebEngine version.\n \n         This is called if we don't want to fully initialize QtWebEngine (so\n@@ -660,14 +758,16 @@ class WebEngineVersions:\n         a PyQtWebEngine-Qt{,5} package from PyPI, so we could query its exact version.\n         \"\"\"\n         parsed = utils.VersionNumber.parse(pyqt_webengine_qt_version)\n+        chromium, chromium_security = cls._infer_chromium_version(parsed)\n         return cls(\n             webengine=parsed,\n-            chromium=cls._infer_chromium_version(parsed),\n-            source='importlib',\n+            chromium=chromium,\n+            chromium_security=chromium_security,\n+            source=source,\n         )\n \n     @classmethod\n-    def from_pyqt(cls, pyqt_webengine_version: str) -&gt; 'WebEngineVersions':\n+    def from_pyqt(cls, pyqt_webengine_version: str, source: str = \"PyQt\") -&gt; 'WebEngineVersions':\n         \"\"\"Get the versions based on the PyQtWebEngine version.\n \n         This is the \"last resort\" if we don't want to fully initialize QtWebEngine (so\n@@ -705,22 +805,12 @@ class WebEngineVersions:\n             if frozen:\n                 parsed = utils.VersionNumber(5, 15, 2)\n \n-        return cls(\n-            webengine=parsed,\n-            chromium=cls._infer_chromium_version(parsed),\n-            source='PyQt',\n-        )\n-\n-    @classmethod\n-    def from_qt(cls, qt_version: str, *, source: str = 'Qt') -&gt; 'WebEngineVersions':\n-        \"\"\"Get the versions based on the Qt version.\n+        chromium, chromium_security = cls._infer_chromium_version(parsed)\n \n-        This is called if we don't have PYQT_WEBENGINE_VERSION, i.e. with PyQt 5.12.\n-        \"\"\"\n-        parsed = utils.VersionNumber.parse(qt_version)\n         return cls(\n             webengine=parsed,\n-            chromium=cls._infer_chromium_version(parsed),\n+            chromium=chromium,\n+            chromium_security=chromium_security,\n             source=source,\n         )\n \n@@ -744,6 +834,35 @@ def qtwebengine_versions(*, avoid_init: bool = False) -&gt; WebEngineVersions:\n     - https://www.chromium.org/developers/calendar\n     - https://chromereleases.googleblog.com/\n     \"\"\"\n+    override = os.environ.get('QUTE_QTWEBENGINE_VERSION_OVERRIDE')\n+    if override is not None:\n+        return WebEngineVersions.from_pyqt(override, source='override')\n+\n+    if machinery.IS_QT6:\n+        try:\n+            from qutebrowser.qt.webenginecore import (\n+                qWebEngineVersion,\n+                qWebEngineChromiumVersion,\n+            )\n+        except ImportError:\n+            pass  # Needs QtWebEngine 6.2+ with PyQtWebEngine 6.3.1+\n+        else:\n+            try:\n+                from qutebrowser.qt.webenginecore import (\n+                    qWebEngineChromiumSecurityPatchVersion,\n+                )\n+                chromium_security = qWebEngineChromiumSecurityPatchVersion()\n+            except ImportError:\n+                chromium_security = None  # Needs QtWebEngine 6.3+\n+\n+            qtwe_version = qWebEngineVersion()\n+            assert qtwe_version is not None\n+            return WebEngineVersions.from_api(\n+                qtwe_version=qtwe_version,\n+                chromium_version=qWebEngineChromiumVersion(),\n+                chromium_security=chromium_security,\n+            )\n+\n     from qutebrowser.browser.webengine import webenginesettings\n \n     if webenginesettings.parsed_user_agent is None and not avoid_init:\n@@ -752,22 +871,17 @@ def qtwebengine_versions(*, avoid_init: bool = False) -&gt; WebEngineVersions:\n     if webenginesettings.parsed_user_agent is not None:\n         return WebEngineVersions.from_ua(webenginesettings.parsed_user_agent)\n \n-    override = os.environ.get('QUTE_QTWEBENGINE_VERSION_OVERRIDE')\n-    if override is not None:\n-        return WebEngineVersions.from_qt(override, source='override')\n-\n     versions = elf.parse_webenginecore()\n     if versions is not None:\n         return WebEngineVersions.from_elf(versions)\n \n     pyqt_webengine_qt_version = _get_pyqt_webengine_qt_version()\n     if pyqt_webengine_qt_version is not None:\n-        return WebEngineVersions.from_importlib(pyqt_webengine_qt_version)\n-\n-    if PYQT_WEBENGINE_VERSION_STR is not None:\n-        return WebEngineVersions.from_pyqt(PYQT_WEBENGINE_VERSION_STR)\n+        return WebEngineVersions.from_webengine(\n+            pyqt_webengine_qt_version, source='importlib')\n \n-    return WebEngineVersions.from_qt(qVersion())  # type: ignore[unreachable]\n+    assert PYQT_WEBENGINE_VERSION_STR is not None\n+    return WebEngineVersions.from_pyqt(PYQT_WEBENGINE_VERSION_STR)\n \n \n def _backend() -&gt; str:\n@@ -816,6 +930,8 @@ def version_info() -&gt; str:\n                         platform.python_version()),\n         'PyQt: {}'.format(PYQT_VERSION_STR),\n         '',\n+        str(machinery.INFO),\n+        '',\n     ]\n \n     lines += _module_versions()\n@@ -829,7 +945,10 @@ def version_info() -&gt; str:\n \n     if objects.qapp:\n         style = objects.qapp.style()\n-        lines.append('Style: {}'.format(style.metaObject().className()))\n+        assert style is not None\n+        metaobj = style.metaObject()\n+        assert metaobj is not None\n+        lines.append('Style: {}'.format(metaobj.className()))\n         lines.append('Platform plugin: {}'.format(objects.qapp.platformName()))\n         lines.append('OpenGL: {}'.format(opengl_info()))\n \n@@ -948,7 +1067,7 @@ def opengl_info() -&gt; Optional[OpenGLInfo]:  # pragma: no cover\n         vendor, version = override.split(', ', maxsplit=1)\n         return OpenGLInfo.parse(vendor=vendor, version=version)\n \n-    old_context = cast(Optional[QOpenGLContext], QOpenGLContext.currentContext())\n+    old_context: Optional[QOpenGLContext] = QOpenGLContext.currentContext()\n     old_surface = None if old_context is None else old_context.surface()\n \n     surface = QOffscreenSurface()\n@@ -974,7 +1093,12 @@ def opengl_info() -&gt; Optional[OpenGLInfo]:  # pragma: no cover\n         vp.setVersion(2, 0)\n \n         try:\n-            vf = ctx.versionFunctions(vp)\n+            if machinery.IS_QT5:\n+                vf = ctx.versionFunctions(vp)\n+            else:\n+                # Qt 6\n+                from qutebrowser.qt.opengl import QOpenGLVersionFunctionsFactory\n+                vf: Any = QOpenGLVersionFunctionsFactory.get(vp, ctx)\n         except ImportError as e:\n             log.init.debug(\"Importing version functions failed: {}\".format(e))\n             return None\n@@ -983,6 +1107,7 @@ def opengl_info() -&gt; Optional[OpenGLInfo]:  # pragma: no cover\n             log.init.debug(\"Getting version functions failed!\")\n             return None\n \n+        # FIXME:mypy PyQt6-stubs issue?\n         vendor = vf.glGetString(vf.GL_VENDOR)\n         version = vf.glGetString(vf.GL_VERSION)\n \ndiff --git a/requirements.txt b/requirements.txt\nindex fe6001ae4..07574e4e6 100644\n--- a/requirements.txt\n+++ b/requirements.txt\n@@ -1,14 +1,13 @@\n # This file is automatically generated by scripts/dev/recompile_requirements.py\n \n adblock==0.6.0\n-colorama==0.4.5\n-importlib-metadata==4.12.0 ; python_version==\"3.7.*\"\n-importlib-resources==5.9.0 ; python_version==\"3.7.*\" or python_version==\"3.8.*\"\n-Jinja2==3.1.2\n-MarkupSafe==2.1.1\n-Pygments==2.13.0\n-pyobjc-core==8.5 ; sys_platform==\"darwin\"\n-pyobjc-framework-Cocoa==8.5 ; sys_platform==\"darwin\"\n-PyYAML==6.0\n-typing_extensions==4.3.0 ; python_version&lt;\"3.8\"\n-zipp==3.8.1\n+colorama==0.4.6\n+importlib_resources==6.4.0 ; python_version==\"3.8.*\"\n+Jinja2==3.1.4\n+MarkupSafe==2.1.5\n+Pygments==2.18.0\n+PyYAML==6.0.1\n+zipp==3.19.0\n+# Unpinned due to recompile_requirements.py limitations\n+pyobjc-core ; sys_platform==\"darwin\"\n+pyobjc-framework-Cocoa ; sys_platform==\"darwin\"\ndiff --git a/scripts/__init__.py b/scripts/__init__.py\nindex 90be1e04d..7985e7bbc 100644\n--- a/scripts/__init__.py\n+++ b/scripts/__init__.py\n@@ -1,3 +1 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n \"\"\"Various utility scripts.\"\"\"\ndiff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py\nindex ba8493247..1d0249d9a 100755\n--- a/scripts/asciidoc2html.py\n+++ b/scripts/asciidoc2html.py\n@@ -1,26 +1,13 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Generate the html documentation based on the asciidoc files.\"\"\"\n \n-from typing import List, Optional\n+from typing import Optional\n import re\n import os\n import sys\n@@ -48,13 +35,7 @@ class AsciiDoc:\n         'install', 'stacktrace'\n     ]\n \n-    def __init__(self,\n-                 asciidoc: Optional[str],\n-                 asciidoc_python: Optional[str],\n-                 website: Optional[str]) -&gt; None:\n-        self._cmd: Optional[List[str]] = None\n-        self._asciidoc = asciidoc\n-        self._asciidoc_python = asciidoc_python\n+    def __init__(self, website: Optional[str]) -&gt; None:\n         self._website = website\n         self._homedir: Optional[pathlib.Path] = None\n         self._themedir: Optional[pathlib.Path] = None\n@@ -63,7 +44,6 @@ class AsciiDoc:\n \n     def prepare(self) -&gt; None:\n         \"\"\"Get the asciidoc command and create the homedir to use.\"\"\"\n-        self._cmd = self._get_asciidoc_cmd()\n         self._homedir = pathlib.Path(tempfile.mkdtemp())\n         self._themedir = self._homedir / '.asciidoc' / 'themes' / 'qute'\n         self._tempdir = self._homedir / 'tmp'\n@@ -73,7 +53,7 @@ class AsciiDoc:\n     def cleanup(self) -&gt; None:\n         \"\"\"Clean up the temporary home directory for asciidoc.\"\"\"\n         if self._homedir is not None and not self._failed:\n-            shutil.rmtree(str(self._homedir))\n+            shutil.rmtree(self._homedir)\n \n     def build(self) -&gt; None:\n         \"\"\"Build either the website or the docs.\"\"\"\n@@ -91,12 +71,15 @@ class AsciiDoc:\n             dst = DOC_DIR / (src.stem + \".html\")\n             files.append((src, dst))\n \n-        # patch image links to use local copy\n         replacements = [\n-            (\"https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png\",\n+            # patch image links to use local copy\n+            (\"https://raw.githubusercontent.com/qutebrowser/qutebrowser/main/doc/img/cheatsheet-big.png\",\n              \"qute://help/img/cheatsheet-big.png\"),\n-            (\"https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-small.png\",\n-             \"qute://help/img/cheatsheet-small.png\")\n+            (\"https://raw.githubusercontent.com/qutebrowser/qutebrowser/main/doc/img/cheatsheet-small.png\",\n+             \"qute://help/img/cheatsheet-small.png\"),\n+\n+            # patch relative links to work with qute://help flat structure\n+            (\"link:../\", \"link:\"),\n         ]\n         asciidoc_args = ['-a', 'source-highlighter=pygments']\n \n@@ -119,7 +102,7 @@ class AsciiDoc:\n         for filename in ['cheatsheet-big.png', 'cheatsheet-small.png']:\n             src = REPO_ROOT / 'doc' / 'img' / filename\n             dst = dst_path / filename\n-            shutil.copy(str(src), str(dst))\n+            shutil.copy(src, dst)\n \n     def _build_website_file(self, root: pathlib.Path, filename: str) -&gt; None:\n         \"\"\"Build a single website file.\"\"\"\n@@ -131,7 +114,7 @@ class AsciiDoc:\n \n         assert self._tempdir is not None    # for mypy\n         modified_src = self._tempdir / src.name\n-        shutil.copy(str(REPO_ROOT / 'www' / 'header.asciidoc'), modified_src)\n+        shutil.copy(REPO_ROOT / 'www' / 'header.asciidoc', modified_src)\n \n         outfp = io.StringIO()\n \n@@ -224,26 +207,6 @@ class AsciiDoc:\n             except FileExistsError:\n                 pass\n \n-    def _get_asciidoc_cmd(self) -&gt; List[str]:\n-        \"\"\"Try to find out what commandline to use to invoke asciidoc.\"\"\"\n-        if self._asciidoc is not None:\n-            python = (sys.executable if self._asciidoc_python is None\n-                      else self._asciidoc_python)\n-            return [python, self._asciidoc]\n-\n-        for executable in ['asciidoc', 'asciidoc.py']:\n-            try:\n-                subprocess.run([executable, '--version'],\n-                               stdout=subprocess.DEVNULL,\n-                               stderr=subprocess.DEVNULL,\n-                               check=True)\n-            except OSError:\n-                pass\n-            else:\n-                return [executable]\n-\n-        raise FileNotFoundError\n-\n     def call(self, src: pathlib.Path, dst: pathlib.Path, *args):\n         \"\"\"Call asciidoc for the given files.\n \n@@ -253,8 +216,7 @@ class AsciiDoc:\n             *args: Additional arguments passed to asciidoc.\n         \"\"\"\n         print(\"Calling asciidoc for {}...\".format(src.name))\n-        assert self._cmd is not None    # for mypy\n-        cmdline = self._cmd[:]\n+        cmdline = [sys.executable, \"-m\", \"asciidoc\"]\n         if dst is not None:\n             cmdline += ['--out-file', str(dst)]\n         cmdline += args\n@@ -281,12 +243,6 @@ def parse_args() -&gt; argparse.Namespace:\n     parser = argparse.ArgumentParser()\n     parser.add_argument('--website', help=\"Build website into a given \"\n                         \"directory.\")\n-    parser.add_argument('--asciidoc', help=\"Full path to asciidoc.py. \"\n-                        \"If not given, it's searched in PATH.\",\n-                        nargs='?')\n-    parser.add_argument('--asciidoc-python', help=\"Python to use for asciidoc.\"\n-                        \"If not given, the current Python interpreter is used.\",\n-                        nargs='?')\n     return parser.parse_args()\n \n \n@@ -298,9 +254,8 @@ def run(**kwargs) -&gt; None:\n     try:\n         asciidoc.prepare()\n     except FileNotFoundError:\n-        utils.print_error(\"Could not find asciidoc! Please install it, or use \"\n-                          \"the --asciidoc argument to point this script to \"\n-                          \"the correct asciidoc.py location!\")\n+        utils.print_error(\"Could not find asciidoc! Please install it, e.g. via \"\n+                          \"pip install -r misc/requirements/requirements-docs.txt\")\n         sys.exit(1)\n \n     try:\n@@ -314,8 +269,7 @@ def main(colors: bool = False) -&gt; None:\n     utils.change_cwd()\n     utils.use_color = colors\n     args = parse_args()\n-    run(asciidoc=args.asciidoc, asciidoc_python=args.asciidoc_python,\n-        website=args.website)\n+    run(website=args.website)\n \n \n if __name__ == '__main__':\ndiff --git a/scripts/dev/Makefile-dmg b/scripts/dev/Makefile-dmg\nindex 8749ce632..48743967d 100644\n--- a/scripts/dev/Makefile-dmg\n+++ b/scripts/dev/Makefile-dmg\n@@ -24,7 +24,7 @@ SOURCE_DIR ?= .\n SOURCE_FILES ?= dist/qutebrowser.app LICENSE\n \n TEMPLATE_DMG ?= template.dmg\n-TEMPLATE_SIZE ?= 500m\n+TEMPLATE_SIZE ?= 750m\n \n ################################################################################\n # DMG building. No editing should be needed beyond this point.\ndiff --git a/scripts/dev/__init__.py b/scripts/dev/__init__.py\nindex 7dc043361..38246491d 100644\n--- a/scripts/dev/__init__.py\n+++ b/scripts/dev/__init__.py\n@@ -1,3 +1 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n \"\"\"Various scripts used for developing qutebrowser.\"\"\"\ndiff --git a/scripts/dev/build_pyqt_wheel.py b/scripts/dev/build_pyqt_wheel.py\nindex b63331341..6c2741523 100644\n--- a/scripts/dev/build_pyqt_wheel.py\n+++ b/scripts/dev/build_pyqt_wheel.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Build updated PyQt wheels.\"\"\"\n \ndiff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py\nindex c175c68a7..40cedc2e8 100755\n--- a/scripts/dev/build_release.py\n+++ b/scripts/dev/build_release.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Build a new release.\"\"\"\n \n@@ -48,6 +34,10 @@ from scripts import utils\n from scripts.dev import update_3rdparty, misc_checks\n \n \n+IS_MACOS = sys.platform == 'darwin'\n+IS_WINDOWS = os.name == 'nt'\n+\n+\n @dataclasses.dataclass\n class Artifact:\n \n@@ -96,15 +86,10 @@ def call_tox(\n         env=env, check=True)\n \n \n-def run_asciidoc2html(args: argparse.Namespace) -&gt; None:\n+def run_asciidoc2html() -&gt; None:\n     \"\"\"Run the asciidoc2html script.\"\"\"\n     utils.print_title(\"Running asciidoc2html.py\")\n-    a2h_args = []\n-    if args.asciidoc is not None:\n-        a2h_args += ['--asciidoc', args.asciidoc]\n-    if args.asciidoc_python is not None:\n-        a2h_args += ['--asciidoc-python', args.asciidoc_python]\n-    call_script('asciidoc2html.py', *a2h_args)\n+    call_script('asciidoc2html.py')\n \n \n def _maybe_remove(path: pathlib.Path) -&gt; None:\n@@ -134,48 +119,70 @@ def _smoke_test_run(\n         '--temp-basedir',\n         *args,\n         'about:blank',\n-        ':later 500 quit',\n+        ':cmd-later 500 quit',\n     ]\n     return subprocess.run(argv, check=True, capture_output=True)\n \n \n-def smoke_test(executable: pathlib.Path, debug: bool) -&gt; None:\n+def smoke_test(executable: pathlib.Path, debug: bool, qt5: bool) -&gt; None:\n     \"\"\"Try starting the given qutebrowser executable.\"\"\"\n     stdout_whitelist = []\n     stderr_whitelist = [\n         # PyInstaller debug output\n         r'\\[.*\\] PyInstaller Bootloader .*',\n         r'\\[.*\\] LOADER: .*',\n-\n-        # https://github.com/qutebrowser/qutebrowser/issues/4919\n-        (r'objc\\[.*\\]: .* One of the two will be used\\. '\n-         r'Which one is undefined\\.'),\n-        (r'QCoreApplication::applicationDirPath: Please instantiate the '\n-         r'QApplication object first'),\n-        (r'\\[.*:ERROR:mach_port_broker.mm\\(48\\)\\] bootstrap_look_up '\n-         r'org\\.chromium\\.Chromium\\.rohitfork\\.1: Permission denied \\(1100\\)'),\n-        (r'\\[.*:ERROR:mach_port_broker.mm\\(43\\)\\] bootstrap_look_up: '\n-         r'Unknown service name \\(1102\\)'),\n-\n-        (r'[0-9:]* WARNING: The available OpenGL surface format was either not '\n-         r'version 3\\.2 or higher or not a Core Profile\\.'),\n-        r'Chromium on macOS will fall back to software rendering in this case\\.',\n-        r'Hardware acceleration and features such as WebGL will not be available\\.',\n-        r'Unable to create basic Accelerated OpenGL renderer\\.',\n-        r'Core Image is now using the software OpenGL renderer\\. This will be slow\\.',\n-\n-        # Windows N:\n-        # https://github.com/microsoft/playwright/issues/2901\n-        (r'\\[.*:ERROR:dxva_video_decode_accelerator_win.cc\\(\\d+\\)\\] '\n-         r'DXVAVDA fatal error: could not LoadLibrary: .*: The specified '\n-         r'module could not be found. \\(0x7E\\)'),\n-\n-        # https://github.com/qutebrowser/qutebrowser/issues/3719\n-        '[0-9:]* ERROR: Load error: ERR_FILE_NOT_FOUND',\n-\n-        # macOS 11\n-        (r'[0-9:]* WARNING: Failed to load libssl/libcrypto\\.'),\n     ]\n+    if IS_MACOS:\n+        stderr_whitelist.extend([\n+            # macOS on Qt 5.15\n+            # https://github.com/qutebrowser/qutebrowser/issues/4919\n+            (r'objc\\[.*\\]: .* One of the two will be used\\. '\n+            r'Which one is undefined\\.'),\n+            (r'QCoreApplication::applicationDirPath: Please instantiate the '\n+            r'QApplication object first'),\n+            (r'\\[.*:ERROR:mach_port_broker.mm\\(48\\)\\] bootstrap_look_up '\n+            r'org\\.chromium\\.Chromium\\.rohitfork\\.1: Permission denied \\(1100\\)'),\n+            (r'\\[.*:ERROR:mach_port_broker.mm\\(43\\)\\] bootstrap_look_up: '\n+            r'Unknown service name \\(1102\\)'),\n+\n+            # macOS on Qt 5.15\n+            (r'[0-9:]* WARNING: The available OpenGL surface format was either not '\n+            r'version 3\\.2 or higher or not a Core Profile\\.'),\n+            r'Chromium on macOS will fall back to software rendering in this case\\.',\n+            r'Hardware acceleration and features such as WebGL will not be available\\.',\n+            r'Unable to create basic Accelerated OpenGL renderer\\.',\n+            r'Core Image is now using the software OpenGL renderer\\. This will be slow\\.',\n+\n+            # https://github.com/qutebrowser/qutebrowser/issues/3719\n+            '[0-9:]* ERROR: Load error: ERR_FILE_NOT_FOUND',\n+\n+            # macOS 11\n+            (r'[0-9:]* WARNING: Failed to load libssl/libcrypto\\.'),\n+\n+            # macOS?\n+            (r'\\[.*:ERROR:command_buffer_proxy_impl.cc\\([0-9]*\\)\\] '\n+            r'ContextResult::kTransientFailure: Failed to send '\n+            r'.*CreateCommandBuffer\\.'),\n+        ])\n+        if not qt5:\n+            stderr_whitelist.extend([\n+                # FIXME:qt6 Qt 6.3 on macOS\n+                r'[0-9:]* WARNING: Incompatible version of OpenSSL',\n+                r'[0-9:]* WARNING: Qt WebEngine resources not found at .*',\n+                (r'[0-9:]* WARNING: Installed Qt WebEngine locales directory not found at '\n+                r'location /qtwebengine_locales\\. Trying application directory\\.\\.\\.'),\n+                # Qt 6.7, only seen on macos for some reason\n+                (r'.*Path override failed for key base::DIR_APP_DICTIONARIES '\n+                 r\"and path '.*/qtwebengine_dictionaries'\"),\n+            ])\n+    elif IS_WINDOWS:\n+        stderr_whitelist.extend([\n+            # Windows N:\n+            # https://github.com/microsoft/playwright/issues/2901\n+            (r'\\[.*:ERROR:dxva_video_decode_accelerator_win.cc\\(\\d+\\)\\] '\n+            r'DXVAVDA fatal error: could not LoadLibrary: .*: The specified '\n+            r'module could not be found. \\(0x7E\\)'),\n+        ])\n \n     proc = _smoke_test_run(executable)\n     if debug:\n@@ -228,7 +235,7 @@ def smoke_test(executable: pathlib.Path, debug: bool) -&gt; None:\n                 \"\",\n             ]\n \n-        raise Exception(\"\\n\".join(lines))\n+        raise Exception(\"\\n\".join(lines))  # pylint: disable=broad-exception-raised\n \n \n def verify_windows_exe(exe_path: pathlib.Path) -&gt; None:\n@@ -238,58 +245,9 @@ def verify_windows_exe(exe_path: pathlib.Path) -&gt; None:\n     assert pe.verify_checksum()\n \n \n-def patch_mac_app() -&gt; None:\n-    \"\"\"Patch .app to save some space and make it signable.\"\"\"\n-    dist_path = pathlib.Path('dist')\n-    app_path = dist_path / 'qutebrowser.app'\n-\n-    contents_path = app_path / 'Contents'\n-    macos_path = contents_path / 'MacOS'\n-    resources_path = contents_path / 'Resources'\n-    pyqt_path = macos_path / 'PyQt5'\n-\n-    # Replace some duplicate files by symlinks\n-    framework_path = pyqt_path / 'Qt5' / 'lib' / 'QtWebEngineCore.framework'\n-\n-    core_lib = framework_path / 'Versions' / '5' / 'QtWebEngineCore'\n-    core_lib.unlink()\n-    core_target = pathlib.Path(*[os.pardir] * 7, 'MacOS', 'QtWebEngineCore')\n-    core_lib.symlink_to(core_target)\n-\n-    framework_resource_path = framework_path / 'Resources'\n-    for file_path in framework_resource_path.iterdir():\n-        target = pathlib.Path(*[os.pardir] * 5, file_path.name)\n-        if file_path.is_dir():\n-            shutil.rmtree(file_path)\n-        else:\n-            file_path.unlink()\n-        file_path.symlink_to(target)\n-\n-    # Move stuff around to make things signable on macOS\n-    # See https://github.com/pyinstaller/pyinstaller/issues/6612\n-    pyqt_path_dest = resources_path / pyqt_path.name\n-    shutil.move(pyqt_path, pyqt_path_dest)\n-    pyqt_path_target = pathlib.Path(\"..\") / pyqt_path_dest.relative_to(contents_path)\n-    pyqt_path.symlink_to(pyqt_path_target)\n-\n-    for path in macos_path.glob(\"Qt*\"):\n-        link_path = resources_path / path.name\n-        target_path = pathlib.Path(\"..\") / path.relative_to(contents_path)\n-        link_path.symlink_to(target_path)\n-\n-\n-def sign_mac_app() -&gt; None:\n+def verify_mac_app() -&gt; None:\n     \"\"\"Re-sign and verify the Mac .app.\"\"\"\n     app_path = pathlib.Path('dist') / 'qutebrowser.app'\n-    subprocess.run([\n-        'codesign',\n-        '-s', '-',\n-        '--force',\n-        '--timestamp',\n-        '--deep',\n-        '--verbose',\n-        app_path,\n-    ], check=True)\n     subprocess.run([\n         'codesign',\n         '--verify',\n@@ -308,6 +266,7 @@ def _mac_bin_path(base: pathlib.Path) -&gt; pathlib.Path:\n def build_mac(\n     *,\n     gh_token: Optional[str],\n+    qt5: bool,\n     skip_packaging: bool,\n     debug: bool,\n ) -&gt; List[Artifact]:\n@@ -322,21 +281,18 @@ def build_mac(\n         shutil.rmtree(d, ignore_errors=True)\n \n     utils.print_title(\"Updating 3rdparty content\")\n-    # FIXME:qt6 Use modern PDF.js version here\n-    update_3rdparty.run(ace=False, pdfjs=True, legacy_pdfjs=True, fancy_dmg=False,\n+    update_3rdparty.run(ace=False, pdfjs=True, legacy_pdfjs=qt5, fancy_dmg=False,\n                         gh_token=gh_token)\n \n     utils.print_title(\"Building .app via pyinstaller\")\n-    call_tox('pyinstaller-64', '-r', debug=debug)\n-    utils.print_title(\"Patching .app\")\n-    patch_mac_app()\n-    utils.print_title(\"Re-signing .app\")\n-    sign_mac_app()\n+    call_tox(f'pyinstaller{\"-qt5\" if qt5 else \"\"}', '-r', debug=debug)\n+    utils.print_title(\"Verifying .app\")\n+    verify_mac_app()\n \n     dist_path = pathlib.Path(\"dist\")\n \n     utils.print_title(\"Running pre-dmg smoke test\")\n-    smoke_test(_mac_bin_path(dist_path), debug=debug)\n+    smoke_test(_mac_bin_path(dist_path), debug=debug, qt5=qt5)\n \n     if skip_packaging:\n         return []\n@@ -346,6 +302,7 @@ def build_mac(\n     subprocess.run(['make', '-f', dmg_makefile_path], check=True)\n \n     suffix = \"-debug\" if debug else \"\"\n+    suffix += \"-qt5\" if qt5 else \"\"\n     dmg_path = dist_path / f'qutebrowser-{qutebrowser.__version__}{suffix}.dmg'\n     pathlib.Path('qutebrowser.dmg').rename(dmg_path)\n \n@@ -357,7 +314,7 @@ def build_mac(\n             subprocess.run(['hdiutil', 'attach', dmg_path,\n                             '-mountpoint', tmp_path], check=True)\n             try:\n-                smoke_test(_mac_bin_path(tmp_path), debug=debug)\n+                smoke_test(_mac_bin_path(tmp_path), debug=debug, qt5=qt5)\n             finally:\n                 print(\"Waiting 10s for dmg to be detachable...\")\n                 time.sleep(10)\n@@ -374,18 +331,14 @@ def build_mac(\n     ]\n \n \n-def _get_windows_python_path(x64: bool) -&gt; pathlib.Path:\n+def _get_windows_python_path() -&gt; pathlib.Path:\n     \"\"\"Get the path to Python.exe on Windows.\"\"\"\n     parts = str(sys.version_info.major), str(sys.version_info.minor)\n     ver = ''.join(parts)\n     dot_ver = '.'.join(parts)\n \n-    if x64:\n-        path = rf'SOFTWARE\\Python\\PythonCore\\{dot_ver}\\InstallPath'\n-        fallback = pathlib.Path('C:', f'Python{ver}', 'python.exe')\n-    else:\n-        path = rf'SOFTWARE\\WOW6432Node\\Python\\PythonCore\\{dot_ver}-32\\InstallPath'\n-        fallback = pathlib.Path('C:', f'Python{ver}-32', 'python.exe')\n+    path = rf'SOFTWARE\\Python\\PythonCore\\{dot_ver}\\InstallPath'\n+    fallback = pathlib.Path('C:', f'Python{ver}', 'python.exe')\n \n     try:\n         key = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, path)\n@@ -395,126 +348,114 @@ def _get_windows_python_path(x64: bool) -&gt; pathlib.Path:\n \n \n def _build_windows_single(\n-    *, x64: bool,\n+    *,\n+    qt5: bool,\n     skip_packaging: bool,\n     debug: bool,\n ) -&gt; List[Artifact]:\n-    \"\"\"Build on Windows for a single architecture.\"\"\"\n-    human_arch = '64-bit' if x64 else '32-bit'\n-    utils.print_title(f\"Running pyinstaller {human_arch}\")\n+    \"\"\"Build on Windows for a single build type.\"\"\"\n+    utils.print_title(\"Running pyinstaller\")\n     dist_path = pathlib.Path(\"dist\")\n \n-    arch = \"x64\" if x64 else \"x86\"\n-    out_path = dist_path / f'qutebrowser-{qutebrowser.__version__}-{arch}'\n+    out_path = dist_path / f'qutebrowser-{qutebrowser.__version__}'\n     _maybe_remove(out_path)\n \n-    python = _get_windows_python_path(x64=x64)\n-    call_tox(f'pyinstaller-{\"64\" if x64 else \"32\"}', '-r', python=python, debug=debug)\n+    python = _get_windows_python_path()\n+    # FIXME:qt6 does this regress 391623d5ec983ecfc4512c7305c4b7a293ac3872?\n+    suffix = \"-qt5\" if qt5 else \"\"\n+    call_tox(f'pyinstaller{suffix}', '-r', python=python, debug=debug)\n \n     out_pyinstaller = dist_path / \"qutebrowser\"\n     shutil.move(out_pyinstaller, out_path)\n     exe_path = out_path / 'qutebrowser.exe'\n \n-    utils.print_title(f\"Verifying {human_arch} exe\")\n+    utils.print_title(\"Verifying exe\")\n     verify_windows_exe(exe_path)\n \n-    utils.print_title(f\"Running {human_arch} smoke test\")\n-    smoke_test(exe_path, debug=debug)\n+    utils.print_title(\"Running smoke test\")\n+    smoke_test(exe_path, debug=debug, qt5=qt5)\n \n     if skip_packaging:\n         return []\n \n-    utils.print_title(f\"Packaging {human_arch}\")\n+    utils.print_title(\"Packaging\")\n     return _package_windows_single(\n-        nsis_flags=[] if x64 else ['/DX86'],\n         out_path=out_path,\n-        filename_arch='amd64' if x64 else 'win32',\n-        desc_arch=human_arch,\n-        desc_suffix='' if x64 else ' (only for 32-bit Windows!)',\n         debug=debug,\n+        qt5=qt5,\n     )\n \n \n def build_windows(\n     *, gh_token: str,\n     skip_packaging: bool,\n-    only_32bit: bool,\n-    only_64bit: bool,\n+    qt5: bool,\n     debug: bool,\n ) -&gt; List[Artifact]:\n     \"\"\"Build windows executables/setups.\"\"\"\n     utils.print_title(\"Updating 3rdparty content\")\n-    # FIXME:qt6 Use modern PDF.js version here\n-    update_3rdparty.run(nsis=True, ace=False, pdfjs=True, legacy_pdfjs=True,\n+    update_3rdparty.run(nsis=True, ace=False, pdfjs=True, legacy_pdfjs=qt5,\n                         fancy_dmg=False, gh_token=gh_token)\n \n     utils.print_title(\"Building Windows binaries\")\n \n-    artifacts = []\n-\n     from scripts.dev import gen_versioninfo\n     utils.print_title(\"Updating VersionInfo file\")\n     gen_versioninfo.main()\n \n-    if not only_32bit:\n-        artifacts += _build_windows_single(\n-            x64=True,\n-            skip_packaging=skip_packaging,\n-            debug=debug,\n-        )\n-    if not only_64bit:\n-        artifacts += _build_windows_single(\n-            x64=False,\n-            skip_packaging=skip_packaging,\n-            debug=debug,\n-        )\n-\n+    artifacts = _build_windows_single(\n+        skip_packaging=skip_packaging,\n+        debug=debug,\n+        qt5=qt5,\n+    )\n     return artifacts\n \n \n def _package_windows_single(\n     *,\n-    nsis_flags: List[str],\n     out_path: pathlib.Path,\n-    desc_arch: str,\n-    desc_suffix: str,\n-    filename_arch: str,\n     debug: bool,\n+    qt5: bool,\n ) -&gt; List[Artifact]:\n     \"\"\"Build the given installer/zip for windows.\"\"\"\n     artifacts = []\n \n     dist_path = pathlib.Path(\"dist\")\n-    utils.print_subtitle(f\"Building {desc_arch} installer...\")\n+    utils.print_subtitle(\"Building installer...\")\n     subprocess.run(['makensis.exe',\n-                    f'/DVERSION={qutebrowser.__version__}', *nsis_flags,\n+                    f'/DVERSION={qutebrowser.__version__}',\n+                    f'/DQT5={qt5}',\n                     'misc/nsis/qutebrowser.nsi'], check=True)\n \n     name_parts = [\n         'qutebrowser',\n         str(qutebrowser.__version__),\n-        filename_arch,\n     ]\n     if debug:\n         name_parts.append('debug')\n+    if qt5:\n+        name_parts.append('qt5')\n+\n+    name_parts.append('amd64')  # FIXME:qt6 temporary until new installer\n     name = '-'.join(name_parts) + '.exe'\n \n     artifacts.append(Artifact(\n         path=dist_path / name,\n         mimetype='application/vnd.microsoft.portable-executable',\n-        description=f'Windows {desc_arch} installer{desc_suffix}',\n+        description='Windows installer',\n     ))\n \n-    utils.print_subtitle(f\"Zipping {desc_arch} standalone...\")\n+    utils.print_subtitle(\"Zipping standalone...\")\n     zip_name_parts = [\n         'qutebrowser',\n         str(qutebrowser.__version__),\n         'windows',\n         'standalone',\n-        filename_arch,\n     ]\n     if debug:\n         zip_name_parts.append('debug')\n+    if qt5:\n+        zip_name_parts.append('qt5')\n     zip_name = '-'.join(zip_name_parts) + '.zip'\n \n     zip_path = dist_path / zip_name\n@@ -522,7 +463,7 @@ def _package_windows_single(\n     artifacts.append(Artifact(\n         path=zip_path,\n         mimetype='application/zip',\n-        description=f'Windows {desc_arch} standalone{desc_suffix}',\n+        description='Windows standalone',\n     ))\n \n     return artifacts\n@@ -580,9 +521,17 @@ def build_sdist() -&gt; List[Artifact]:\n def test_makefile() -&gt; None:\n     \"\"\"Make sure the Makefile works correctly.\"\"\"\n     utils.print_title(\"Testing makefile\")\n+    a2x_path = pathlib.Path(sys.executable).parent / 'a2x'\n+    assert a2x_path.exists(), a2x_path\n     with tempfile.TemporaryDirectory() as tmpdir:\n-        subprocess.run(['make', '-f', 'misc/Makefile',\n-                        f'DESTDIR={tmpdir}', 'install'], check=True)\n+        subprocess.run(\n+            [\n+                'make', '-f', 'misc/Makefile',\n+                f'DESTDIR={tmpdir}', f'A2X={a2x_path}',\n+                'install'\n+            ],\n+            check=True,\n+        )\n \n \n def read_github_token(\n@@ -593,12 +542,15 @@ def read_github_token(\n     if arg_token is not None:\n         return arg_token\n \n+    if \"GITHUB_TOKEN\" in os.environ:\n+        return os.environ[\"GITHUB_TOKEN\"]\n+\n     token_path = pathlib.Path.home() / '.gh_token'\n     if not token_path.exists():\n         if optional:\n             return None\n         else:\n-            raise Exception(\n+            raise Exception(  # pylint: disable=broad-exception-raised\n                 \"GitHub token needed, but ~/.gh_token not found, \"\n                 \"and --gh-token not given.\")\n \n@@ -606,27 +558,40 @@ def read_github_token(\n     return token\n \n \n-def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -&gt; None:\n+def github_upload(\n+    artifacts: List[Artifact],\n+    tag: str,\n+    gh_token: str,\n+    experimental: bool,\n+) -&gt; None:\n     \"\"\"Upload the given artifacts to GitHub.\n \n     Args:\n         artifacts: A list of Artifacts to upload.\n         tag: The name of the release tag\n         gh_token: The GitHub token to use\n+        experimental: Upload to the experiments repo\n     \"\"\"\n+    # pylint: disable=broad-exception-raised\n     import github3\n     import github3.exceptions\n     utils.print_title(\"Uploading to github...\")\n \n     gh = github3.login(token=gh_token)\n-    repo = gh.repository('qutebrowser', 'qutebrowser')\n+\n+    if experimental:\n+        repo = gh.repository('qutebrowser', 'experiments')\n+    else:\n+        repo = gh.repository('qutebrowser', 'qutebrowser')\n \n     release = None  # to satisfy pylint\n     for release in repo.releases():\n         if release.tag_name == tag:\n             break\n     else:\n-        raise Exception(f\"No release found for {tag!r}!\")\n+        releases = \", \".join(r.tag_name for r in repo.releases())\n+        raise Exception(\n+            f\"No release found for {tag!r} in {repo.full_name}, found: {releases}\")\n \n     for artifact in artifacts:\n         while True:\n@@ -636,6 +601,10 @@ def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -&gt; None:\n                       if asset.name == artifact.path.name]\n             if assets:\n                 print(f\"Assets already exist: {assets}\")\n+\n+                if utils.ON_CI:\n+                    sys.exit(1)\n+\n                 print(\"Press enter to continue anyways or Ctrl-C to abort.\")\n                 input()\n \n@@ -649,8 +618,13 @@ def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -&gt; None:\n                     )\n             except github3.exceptions.ConnectionError as e:\n                 utils.print_error(f'Failed to upload: {e}')\n-                print(\"Press Enter to retry...\", file=sys.stderr)\n-                input()\n+                if utils.ON_CI:\n+                    print(\"Retrying in 30s...\")\n+                    time.sleep(30)\n+                else:\n+                    print(\"Press Enter to retry...\", file=sys.stderr)\n+                    input()\n+\n                 print(\"Retrying!\")\n \n                 assets = [asset for asset in release.assets()\n@@ -663,10 +637,16 @@ def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -&gt; None:\n                 break\n \n \n-def pypi_upload(artifacts: List[Artifact]) -&gt; None:\n+def pypi_upload(artifacts: List[Artifact], experimental: bool) -&gt; None:\n     \"\"\"Upload the given artifacts to PyPI using twine.\"\"\"\n+    # https://blog.pypi.org/posts/2023-05-23-removing-pgp/\n+    artifacts = [a for a in artifacts if a.mimetype != 'application/pgp-signature']\n+\n     utils.print_title(\"Uploading to PyPI...\")\n-    run_twine('upload', artifacts)\n+    if experimental:\n+        run_twine('upload', artifacts, \"-r\", \"testpypi\")\n+    else:\n+        run_twine('upload', artifacts)\n \n \n def twine_check(artifacts: List[Artifact]) -&gt; None:\n@@ -684,12 +664,6 @@ def main() -&gt; None:\n     parser = argparse.ArgumentParser()\n     parser.add_argument('--skip-docs', action='store_true',\n                         help=\"Don't generate docs\")\n-    parser.add_argument('--asciidoc', help=\"Full path to asciidoc.py. \"\n-                        \"If not given, it's searched in PATH.\",\n-                        nargs='?')\n-    parser.add_argument('--asciidoc-python', help=\"Python to use for asciidoc.\"\n-                        \"If not given, the current Python interpreter is used.\",\n-                        nargs='?')\n     parser.add_argument('--gh-token', help=\"GitHub token to use.\",\n                         nargs='?')\n     parser.add_argument('--upload', action='store_true', required=False,\n@@ -698,12 +672,12 @@ def main() -&gt; None:\n                         help=\"Skip confirmation before uploading.\")\n     parser.add_argument('--skip-packaging', action='store_true', required=False,\n                         help=\"Skip Windows installer/zip generation or macOS DMG.\")\n-    parser.add_argument('--32bit', action='store_true', required=False,\n-                        help=\"Skip Windows 64 bit build.\", dest='only_32bit')\n-    parser.add_argument('--64bit', action='store_true', required=False,\n-                        help=\"Skip Windows 32 bit build.\", dest='only_64bit')\n     parser.add_argument('--debug', action='store_true', required=False,\n                         help=\"Build a debug build.\")\n+    parser.add_argument('--qt5', action='store_true', required=False,\n+                        help=\"Build against PyQt5\")\n+    parser.add_argument('--experimental', action='store_true', required=False,\n+                        help=\"Upload to experiments repo and test PyPI\")\n     args = parser.parse_args()\n     utils.change_cwd()\n \n@@ -716,6 +690,7 @@ def main() -&gt; None:\n         gh_token = read_github_token(args.gh_token)\n     else:\n         gh_token = read_github_token(args.gh_token, optional=True)\n+        assert not args.experimental  # makes no sense without upload\n \n     if not misc_checks.check_git():\n         utils.print_error(\"Refusing to do a release with a dirty git tree\")\n@@ -724,20 +699,20 @@ def main() -&gt; None:\n     if args.skip_docs:\n         pathlib.Path(\"qutebrowser\", \"html\", \"doc\").mkdir(parents=True, exist_ok=True)\n     else:\n-        run_asciidoc2html(args)\n+        run_asciidoc2html()\n \n-    if os.name == 'nt':\n+    if IS_WINDOWS:\n         artifacts = build_windows(\n             gh_token=gh_token,\n             skip_packaging=args.skip_packaging,\n-            only_32bit=args.only_32bit,\n-            only_64bit=args.only_64bit,\n+            qt5=args.qt5,\n             debug=args.debug,\n         )\n-    elif sys.platform == 'darwin':\n+    elif IS_MACOS:\n         artifacts = build_mac(\n             gh_token=gh_token,\n             skip_packaging=args.skip_packaging,\n+            qt5=args.qt5,\n             debug=args.debug,\n         )\n     else:\n@@ -749,14 +724,15 @@ def main() -&gt; None:\n     if args.upload:\n         version_tag = f\"v{qutebrowser.__version__}\"\n \n-        if not args.no_confirm:\n+        if not args.no_confirm and not utils.ON_CI:\n             utils.print_title(f\"Press enter to release {version_tag}...\")\n             input()\n \n         assert gh_token is not None\n-        github_upload(artifacts, version_tag, gh_token=gh_token)\n+        github_upload(\n+            artifacts, version_tag, gh_token=gh_token, experimental=args.experimental)\n         if upload_to_pypi:\n-            pypi_upload(artifacts)\n+            pypi_upload(artifacts, experimental=args.experimental)\n     else:\n         print()\n         utils.print_title(\"Artifacts\")\ndiff --git a/scripts/dev/change_release.py b/scripts/dev/change_release.py\nindex b033b04ea..26d79ff24 100644\n--- a/scripts/dev/change_release.py\n+++ b/scripts/dev/change_release.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Change a description of a GitHub release.\"\"\"\n \ndiff --git a/scripts/dev/changelog_urls.json b/scripts/dev/changelog_urls.json\nindex bcf30fc78..645bb6385 100644\n--- a/scripts/dev/changelog_urls.json\n+++ b/scripts/dev/changelog_urls.json\n@@ -1,14 +1,11 @@\n {\n-  \"pyparsing\": \"https://github.com/pyparsing/pyparsing/blob/master/CHANGES\",\n-  \"pylint\": \"https://pylint.pycqa.org/en/latest/whatsnew/2/index.html\",\n+  \"pylint\": \"https://pylint.pycqa.org/en/latest/whatsnew/3/index.html\",\n   \"tomlkit\": \"https://github.com/sdispater/tomlkit/blob/master/CHANGELOG.md\",\n   \"dill\": \"https://github.com/uqfoundation/dill/commits/master\",\n-  \"isort\": \"https://pycqa.github.io/isort/CHANGELOG/\",\n-  \"lazy-object-proxy\": \"https://github.com/ionelmc/python-lazy-object-proxy/blob/master/CHANGELOG.rst\",\n+  \"isort\": \"https://github.com/PyCQA/isort/blob/main/CHANGELOG.md\",\n   \"mccabe\": \"https://github.com/PyCQA/mccabe#changes\",\n   \"pytest-cov\": \"https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst\",\n   \"pytest-xdist\": \"https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst\",\n-  \"pytest-forked\": \"https://github.com/pytest-dev/pytest-forked/blob/master/CHANGELOG.rst\",\n   \"pytest-xvfb\": \"https://github.com/The-Compiler/pytest-xvfb/blob/master/CHANGELOG.rst\",\n   \"PyVirtualDisplay\": \"https://github.com/ponty/PyVirtualDisplay/commits/master\",\n   \"execnet\": \"https://execnet.readthedocs.io/en/latest/changelog.html\",\n@@ -24,14 +21,18 @@\n   \"soupsieve\": \"https://facelessuser.github.io/soupsieve/about/changelog/\",\n   \"Flask\": \"https://flask.palletsprojects.com/en/latest/changes/\",\n   \"Mako\": \"https://docs.makotemplates.org/en/latest/changelog.html\",\n-  \"glob2\": \"https://github.com/miracle2k/python-glob2/blob/master/CHANGES\",\n   \"hypothesis\": \"https://hypothesis.readthedocs.io/en/latest/changes.html\",\n-  \"exceptiongroup\": \"https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst\",\n   \"mypy\": \"https://mypy-lang.blogspot.com/\",\n-  \"types-PyYAML\": \"https://github.com/python/typeshed/commits/master/stubs/PyYAML\",\n+  \"types-PyYAML\": \"https://github.com/python/typeshed/commits/main/stubs/PyYAML\",\n+  \"types-colorama\": \"https://github.com/python/typeshed/commits/main/stubs/colorama\",\n+  \"types-docutils\": \"https://github.com/python/typeshed/commits/main/stubs/docutils\",\n+  \"types-Pygments\": \"https://github.com/python/typeshed/commits/main/stubs/Pygments\",\n+  \"types-setuptools\": \"https://github.com/python/typeshed/commits/main/stubs/setuptools\",\n   \"pytest\": \"https://docs.pytest.org/en/latest/changelog.html\",\n   \"iniconfig\": \"https://github.com/pytest-dev/iniconfig/blob/master/CHANGELOG\",\n   \"tox\": \"https://tox.readthedocs.io/en/latest/changelog.html\",\n+  \"cachetools\": \"https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst\",\n+  \"pyproject-api\": \"https://github.com/tox-dev/pyproject-api/releases\",\n   \"PyYAML\": \"https://github.com/yaml/pyyaml/blob/master/CHANGES\",\n   \"pytest-bdd\": \"https://github.com/pytest-dev/pytest-bdd/blob/master/CHANGES.rst\",\n   \"snowballstemmer\": \"https://github.com/snowballstem/snowball/blob/master/NEWS\",\n@@ -42,15 +43,14 @@\n   \"Jinja2\": \"https://jinja.palletsprojects.com/en/latest/changes/\",\n   \"MarkupSafe\": \"https://markupsafe.palletsprojects.com/en/latest/changes/\",\n   \"flake8\": \"https://github.com/PyCQA/flake8/tree/main/docs/source/release-notes\",\n-  \"flake8-docstrings\": \"https://pypi.org/project/flake8-docstrings/\",\n+  \"flake8-docstrings\": \"https://github.com/PyCQA/flake8-docstrings/blob/main/HISTORY.rst\",\n   \"flake8-debugger\": \"https://github.com/JBKahn/flake8-debugger/\",\n-  \"flake8-builtins\": \"https://github.com/gforcada/flake8-builtins/blob/master/CHANGES.rst\",\n+  \"flake8-builtins\": \"https://github.com/gforcada/flake8-builtins/blob/main/CHANGES.rst\",\n   \"flake8-bugbear\": \"https://github.com/PyCQA/flake8-bugbear#change-log\",\n-  \"flake8-tidy-imports\": \"https://github.com/adamchainz/flake8-tidy-imports/blob/main/HISTORY.rst\",\n+  \"flake8-tidy-imports\": \"https://github.com/adamchainz/flake8-tidy-imports/blob/main/CHANGELOG.rst\",\n   \"flake8-tuple\": \"https://github.com/ar4s/flake8_tuple/blob/master/HISTORY.rst\",\n-  \"flake8-comprehensions\": \"https://github.com/adamchainz/flake8-comprehensions/blob/main/HISTORY.rst\",\n-  \"flake8-copyright\": \"https://github.com/savoirfairelinux/flake8-copyright/blob/master/CHANGELOG.rst\",\n-  \"flake8-deprecated\": \"https://github.com/gforcada/flake8-deprecated/blob/master/CHANGES.rst\",\n+  \"flake8-comprehensions\": \"https://github.com/adamchainz/flake8-comprehensions/blob/main/CHANGELOG.rst\",\n+  \"flake8-deprecated\": \"https://github.com/gforcada/flake8-deprecated/blob/main/CHANGES.rst\",\n   \"flake8-future-import\": \"https://github.com/xZise/flake8-future-import#changes\",\n   \"flake8-string-format\": \"https://github.com/xZise/flake8-string-format#changes\",\n   \"flake8-plugin-utils\": \"https://github.com/afonasev/flake8-plugin-utils#change-log\",\n@@ -58,7 +58,7 @@\n   \"pep8-naming\": \"https://github.com/PyCQA/pep8-naming/blob/main/CHANGELOG.rst\",\n   \"pycodestyle\": \"https://github.com/PyCQA/pycodestyle/blob/main/CHANGES.txt\",\n   \"pyflakes\": \"https://github.com/PyCQA/pyflakes/blob/master/NEWS.rst\",\n-  \"cffi\": \"https://foss.heptapod.net/pypy/cffi/-/blob/branch/default/doc/source/whatsnew.rst\",\n+  \"cffi\": \"https://github.com/python-cffi/cffi/blob/main/doc/source/whatsnew.rst\",\n   \"astroid\": \"https://github.com/PyCQA/astroid/blob/main/ChangeLog\",\n   \"pytest-instafail\": \"https://github.com/pytest-dev/pytest-instafail/blob/master/CHANGES.rst\",\n   \"coverage\": \"https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst\",\n@@ -78,35 +78,34 @@\n   \"sphinxcontrib-jsmath\": \"https://www.sphinx-doc.org/en/master/changes.html\",\n   \"sphinxcontrib-qthelp\": \"https://www.sphinx-doc.org/en/master/changes.html\",\n   \"sphinxcontrib-serializinghtml\": \"https://www.sphinx-doc.org/en/master/changes.html\",\n-  \"jaraco.functools\": \"https://github.com/jaraco/jaraco.functools/blob/main/CHANGES.rst\",\n   \"parse\": \"https://github.com/r1chardj0n3s/parse#potential-gotchas\",\n-  \"py\": \"https://py.readthedocs.io/en/latest/changelog.html#changelog\",\n   \"Pympler\": \"https://github.com/pympler/pympler/blob/master/CHANGELOG.md\",\n   \"pytest-mock\": \"https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst\",\n   \"pytest-qt\": \"https://github.com/pytest-dev/pytest-qt/blob/master/CHANGELOG.rst\",\n   \"pyinstaller\": \"https://pyinstaller.readthedocs.io/en/stable/CHANGES.html\",\n   \"pyinstaller-hooks-contrib\": \"https://github.com/pyinstaller/pyinstaller-hooks-contrib/blob/master/CHANGELOG.rst\",\n   \"pytest-benchmark\": \"https://pytest-benchmark.readthedocs.io/en/stable/changelog.html\",\n-  \"typed-ast\": \"https://github.com/python/typed_ast/commits/master\",\n   \"docutils\": \"https://docutils.sourceforge.io/RELEASE-NOTES.html\",\n   \"bump2version\": \"https://github.com/c4urself/bump2version/blob/master/CHANGELOG.md\",\n   \"six\": \"https://github.com/benjaminp/six/blob/master/CHANGES\",\n   \"altgraph\": \"https://github.com/ronaldoussoren/altgraph/blob/master/doc/changelog.rst\",\n   \"urllib3\": \"https://github.com/urllib3/urllib3/blob/main/CHANGES.rst\",\n   \"lxml\": \"https://github.com/lxml/lxml/blob/master/CHANGES.txt\",\n-  \"wrapt\": \"https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst\",\n-  \"pep517\": \"https://github.com/pypa/pep517/blob/main/doc/changelog.rst\",\n   \"cryptography\": \"https://cryptography.io/en/latest/changelog.html\",\n-  \"toml\": \"https://github.com/uiri/toml/releases\",\n   \"tomli\": \"https://github.com/hukkin/tomli/blob/master/CHANGELOG.md\",\n   \"PyQt5\": \"https://www.riverbankcomputing.com/news\",\n   \"PyQt5-Qt5\": \"https://www.riverbankcomputing.com/news\",\n   \"PyQtWebEngine\": \"https://www.riverbankcomputing.com/news\",\n   \"PyQtWebEngine-Qt5\": \"https://www.riverbankcomputing.com/news\",\n-  \"PyQt-builder\": \"https://www.riverbankcomputing.com/news\",\n+  \"PyQt-builder\": \"https://pyqt-builder.readthedocs.io/en/stable/releases.html\",\n   \"PyQt5-sip\": \"https://www.riverbankcomputing.com/news\",\n   \"PyQt5-stubs\": \"https://github.com/python-qt-tools/PyQt5-stubs/blob/master/CHANGELOG.md\",\n   \"sip\": \"https://www.riverbankcomputing.com/news\",\n+  \"PyQt6\": \"https://www.riverbankcomputing.com/news\",\n+  \"PyQt6-Qt6\": \"https://www.riverbankcomputing.com/news\",\n+  \"PyQt6-WebEngine\": \"https://www.riverbankcomputing.com/news\",\n+  \"PyQt6-WebEngine-Qt6\": \"https://www.riverbankcomputing.com/news\",\n+  \"PyQt6-sip\": \"https://www.riverbankcomputing.com/news\",\n   \"Pygments\": \"https://pygments.org/docs/changelog/\",\n   \"vulture\": \"https://github.com/jendrikseipp/vulture/blob/main/CHANGELOG.md\",\n   \"distlib\": \"https://github.com/pypa/distlib/blob/master/CHANGES.rst\",\n@@ -118,43 +117,50 @@\n   \"idna\": \"https://github.com/kjd/idna/blob/master/HISTORY.rst\",\n   \"tldextract\": \"https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md\",\n   \"typing_extensions\": \"https://github.com/python/typing_extensions/blob/main/CHANGELOG.md\",\n-  \"diff-cover\": \"https://github.com/Bachmann1234/diff_cover/blob/main/CHANGELOG\",\n-  \"beautifulsoup4\": \"https://bazaar.launchpad.net/~leonardr/beautifulsoup/bs4/view/head:/CHANGELOG\",\n+  \"diff_cover\": \"https://github.com/Bachmann1234/diff_cover/blob/main/CHANGELOG\",\n+  \"beautifulsoup4\": \"https://git.launchpad.net/beautifulsoup/tree/CHANGELOG\",\n   \"check-manifest\": \"https://github.com/mgedmin/check-manifest/blob/master/CHANGES.rst\",\n   \"yamllint\": \"https://github.com/adrienverge/yamllint/blob/master/CHANGELOG.rst\",\n   \"pathspec\": \"https://github.com/cpburnz/python-path-specification/blob/master/CHANGES.rst\",\n-  \"filelock\": \"https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst\",\n+  \"filelock\": \"https://github.com/tox-dev/py-filelock/releases\",\n   \"github3.py\": \"https://github3.readthedocs.io/en/latest/release-notes/index.html\",\n   \"manhole\": \"https://github.com/ionelmc/python-manhole/blob/master/CHANGELOG.rst\",\n-  \"pycparser\": \"https://github.com/eliben/pycparser/blob/master/CHANGES\",\n+  \"pycparser\": \"https://github.com/eliben/pycparser/releases\",\n   \"python-dateutil\": \"https://dateutil.readthedocs.io/en/stable/changelog.html\",\n-  \"platformdirs\": \"https://github.com/platformdirs/platformdirs/blob/main/CHANGES.rst\",\n+  \"platformdirs\": \"https://github.com/platformdirs/platformdirs/releases\",\n   \"pluggy\": \"https://github.com/pytest-dev/pluggy/blob/main/CHANGELOG.rst\",\n   \"mypy-extensions\": \"https://github.com/python/mypy_extensions/commits/master\",\n   \"pyroma\": \"https://github.com/regebro/pyroma/blob/master/CHANGES.txt\",\n   \"adblock\": \"https://github.com/ArniDagur/python-adblock/blob/master/CHANGELOG.md\",\n-  \"importlib-resources\": \"https://importlib-resources.readthedocs.io/en/latest/history.html\",\n-  \"importlib-metadata\": \"https://github.com/python/importlib_metadata/blob/main/CHANGES.rst\",\n-  \"zipp\": \"https://github.com/jaraco/zipp/blob/main/CHANGES.rst\",\n+  \"importlib_resources\": \"https://importlib-resources.readthedocs.io/en/latest/history.html\",\n+  \"importlib_metadata\": \"https://github.com/python/importlib_metadata/blob/main/NEWS.rst\",\n+  \"zipp\": \"https://zipp.readthedocs.io/en/latest/history.html\",\n   \"pip\": \"https://pip.pypa.io/en/stable/news/\",\n   \"wheel\": \"https://wheel.readthedocs.io/en/stable/news.html\",\n   \"setuptools\": \"https://setuptools.readthedocs.io/en/latest/history.html\",\n-  \"future\": \"https://python-future.org/whatsnew.html\",\n   \"pefile\": \"https://github.com/erocarrera/pefile/commits/master\",\n   \"SecretStorage\": \"https://github.com/mitya57/secretstorage/blob/master/changelog\",\n-  \"bleach\": \"https://github.com/mozilla/bleach/blob/main/CHANGES\",\n   \"jeepney\": \"https://gitlab.com/takluyver/jeepney/-/blob/master/docs/release-notes.rst\",\n-  \"keyring\": \"https://github.com/jaraco/keyring/blob/main/CHANGES.rst\",\n+  \"keyring\": \"https://keyring.readthedocs.io/en/latest/history.html\",\n+  \"jaraco.classes\": \"https://jaracoclasses.readthedocs.io/en/latest/history.html\",\n+  \"jaraco.context\": \"https://jaracocontext.readthedocs.io/en/latest/history.html\",\n+  \"jaraco.functools\": \"https://jaracofunctools.readthedocs.io/en/latest/history.html\",\n+  \"backports.tarfile\": \"https://github.com/jaraco/backports.tarfile/blob/main/NEWS.rst\",\n   \"pkginfo\": \"https://bazaar.launchpad.net/~tseaver/pkginfo/trunk/view/head:/CHANGES.txt\",\n-  \"readme-renderer\": \"https://github.com/pypa/readme_renderer/blob/main/CHANGES.rst\",\n+  \"readme_renderer\": \"https://github.com/pypa/readme_renderer/blob/main/CHANGES.rst\",\n   \"requests-toolbelt\": \"https://github.com/requests/toolbelt/blob/master/HISTORY.rst\",\n   \"rfc3986\": \"https://rfc3986.readthedocs.io/en/latest/release-notes/index.html\",\n   \"twine\": \"https://twine.readthedocs.io/en/stable/changelog.html\",\n-  \"webencodings\": \"https://github.com/gsnedders/python-webencodings/commits/master\",\n   \"PyJWT\": \"https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst\",\n-  \"commonmark\": \"https://github.com/readthedocs/commonmark.py/blob/master/CHANGELOG.md\",\n   \"rich\": \"https://github.com/Textualize/rich/blob/master/CHANGELOG.md\",\n-  \"ply\": \"https://github.com/dabeaz/ply/blob/master/CHANGES\",\n   \"pyobjc-core\": \"https://pyobjc.readthedocs.io/en/latest/changelog.html\",\n-  \"pyobjc-framework-Cocoa\": \"https://pyobjc.readthedocs.io/en/latest/changelog.html\"\n+  \"pyobjc-framework-Cocoa\": \"https://pyobjc.readthedocs.io/en/latest/changelog.html\",\n+  \"trove-classifiers\": \"https://github.com/pypa/trove-classifiers/commits/main\",\n+  \"asciidoc\": \"https://asciidoc-py.github.io/CHANGELOG.html\",\n+  \"pyproject_hooks\": \"https://pyproject-hooks.readthedocs.io/en/latest/changelog.html\",\n+  \"markdown-it-py\": \"https://github.com/executablebooks/markdown-it-py/blob/master/CHANGELOG.md\",\n+  \"mdurl\": \"https://github.com/executablebooks/mdurl/commits/master\",\n+  \"blinker\": \"https://blinker.readthedocs.io/en/stable/#changes\",\n+  \"exceptiongroup\": \"https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst\",\n+  \"nh3\": \"https://github.com/messense/nh3/commits/main\"\n }\ndiff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py\nindex 0f8b23554..e1d0d8642 100644\n--- a/scripts/dev/check_coverage.py\n+++ b/scripts/dev/check_coverage.py\n@@ -1,22 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Enforce perfect coverage on some files.\"\"\"\n \n@@ -86,8 +73,8 @@ PERFECT_FILES = [\n      'qutebrowser/browser/history.py'),\n     ('tests/unit/browser/test_pdfjs.py',\n      'qutebrowser/browser/pdfjs.py'),\n-    ('tests/unit/browser/webkit/http/test_http.py',\n-     'qutebrowser/browser/webkit/http.py'),\n+    ('tests/unit/browser/webkit/http/test_httpheaders.py',\n+     'qutebrowser/browser/webkit/httpheaders.py'),\n     # ('tests/unit/browser/webkit/test_webkitelem.py',\n     #  'qutebrowser/browser/webkit/webkitelem.py'),\n     # ('tests/unit/browser/webkit/test_webkitelem.py',\n@@ -136,6 +123,8 @@ PERFECT_FILES = [\n      'qutebrowser/misc/objects.py'),\n     ('tests/unit/misc/test_throttle.py',\n      'qutebrowser/misc/throttle.py'),\n+    ('tests/unit/misc/test_pakjoy.py',\n+     'qutebrowser/misc/pakjoy.py'),\n \n     (None,\n      'qutebrowser/mainwindow/statusbar/keystring.py'),\n@@ -341,10 +330,6 @@ def main_check():\n         print(\"or check https://codecov.io/github/qutebrowser/qutebrowser\")\n         print()\n \n-    if scriptutils.ON_CI:\n-        print(\"Keeping coverage.xml on CI.\")\n-    else:\n-        os.remove('coverage.xml')\n     return 1 if messages else 0\n \n \n@@ -365,7 +350,6 @@ def main_check_all():\n              '--cov-report', 'xml', test_file], check=True)\n         with open('coverage.xml', encoding='utf-8') as f:\n             messages = check(f, [(test_file, src_file)])\n-        os.remove('coverage.xml')\n \n         messages = [msg for msg in messages\n                     if msg.typ == MsgType.insufficient_coverage]\ndiff --git a/scripts/dev/check_doc_changes.py b/scripts/dev/check_doc_changes.py\nindex 5e3a457b6..a2621fbd3 100755\n--- a/scripts/dev/check_doc_changes.py\n+++ b/scripts/dev/check_doc_changes.py\n@@ -1,22 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Check if docs changed and output an error if so.\"\"\"\n \n@@ -33,7 +20,7 @@ from scripts import utils\n code = subprocess.run(['git', '--no-pager', 'diff', '--exit-code', '--stat',\n                        '--', 'doc'], check=False).returncode\n \n-if os.environ.get('GITHUB_REF', 'refs/heads/master') != 'refs/heads/master':\n+if os.environ.get('GITHUB_REF', 'refs/heads/main') != 'refs/heads/main':\n     if code != 0:\n         print(\"Docs changed but ignoring change as we're building a PR\")\n     sys.exit(0)\ndiff --git a/scripts/dev/ci/docker/Dockerfile.j2 b/scripts/dev/ci/docker/Dockerfile.j2\nindex c30141216..d9f636376 100644\n--- a/scripts/dev/ci/docker/Dockerfile.j2\n+++ b/scripts/dev/ci/docker/Dockerfile.j2\n@@ -1,16 +1,33 @@\n FROM archlinux:latest\n \n+RUN pacman-key --init &amp;&amp; pacman-key --populate\n {% if unstable %}\n RUN sed -i '/^# after the header/a[kde-unstable]\\nInclude = /etc/pacman.d/mirrorlist\\n\\n[testing]\\nInclude = /etc/pacman.d/mirrorlist\\n\\n[community-testing]\\nInclude = /etc/pacman.d/mirrorlist' /etc/pacman.conf\n {% endif %}\n-RUN pacman -Suyy --noconfirm \\\n+RUN pacman -Sy --noconfirm archlinux-keyring\n+RUN pacman -Su --noconfirm \\\n     git \\\n+    {% if webengine %}\n     python-tox \\\n     python-distlib \\\n-    qt5-base \\\n-    qt5-declarative \\\n-    {% if webengine %}qt5-webengine python-pyqtwebengine{% else %}qt5-webkit{% endif %} \\\n-    python-pyqt5 \\\n+    {% endif %}\n+    {% if qt6 %}\n+      qt6-base \\\n+      qt6-declarative \\\n+      {% if webengine %}\n+        qt6-webengine python-pyqt6-webengine \\\n+        pdfjs \\\n+      {% else %}{{ 1/0 }}{% endif %}\n+      python-pyqt6 \\\n+    {% else %}\n+      qt5-base \\\n+      qt5-declarative \\\n+      {% if webengine %}\n+        qt5-webengine \\\n+        python-pyqtwebengine \\\n+        python-pyqt5 \\\n+      {% endif %}\n+    {% endif %}\n     xorg-xinit \\\n     xorg-server-xvfb \\\n     ttf-bitstream-vera \\\n@@ -18,6 +35,35 @@ RUN pacman -Suyy --noconfirm \\\n     libyaml \\\n     xorg-xdpyinfo\n \n+{% if not webengine %}\n+RUN pacman -U --noconfirm \\\n+    https://archive.archlinux.org/packages/q/qt5-webkit/qt5-webkit-5.212.0alpha4-18-x86_64.pkg.tar.zst \\\n+    https://archive.archlinux.org/packages/p/python-pyqt5/python-pyqt5-5.15.7-2-x86_64.pkg.tar.zst \\\n+    https://archive.archlinux.org/packages/p/python/python-3.10.10-1-x86_64.pkg.tar.zst \\\n+    https://archive.archlinux.org/packages/i/icu/icu-72.1-2-x86_64.pkg.tar.zst \\\n+    https://archive.archlinux.org/packages/l/libxml2/libxml2-2.10.4-4-x86_64.pkg.tar.zst \\\n+    https://archive.archlinux.org/packages/q/qt5-base/qt5-base-5.15.10%2Bkde%2Br129-3-x86_64.pkg.tar.zst \\\n+    https://archive.archlinux.org/packages/q/qt5-declarative/qt5-declarative-5.15.10%2Bkde%2Br31-1-x86_64.pkg.tar.zst \\\n+    https://archive.archlinux.org/packages/q/qt5-translations/qt5-translations-5.15.10-1-any.pkg.tar.zst \\\n+    https://archive.archlinux.org/packages/q/qt5-sensors/qt5-sensors-5.15.10-1-x86_64.pkg.tar.zst \\\n+    https://archive.archlinux.org/packages/q/qt5-location/qt5-location-5.15.10%2Bkde%2Br5-1-x86_64.pkg.tar.zst \\\n+    https://archive.archlinux.org/packages/q/qt5-webchannel/qt5-webchannel-5.15.10%2Bkde%2Br3-1-x86_64.pkg.tar.zst\n+\n+RUN python3 -m ensurepip\n+RUN python3 -m pip install tox pyqt5-sip\n+{% endif %}\n+\n+{% if qt6 %}\n+  {% set pyqt_module = 'PyQt6' %}\n+{% else %}\n+  {% set pyqt_module = 'PyQt5' %}\n+{% endif %}\n+{% if webengine %}\n+  RUN python3 -c \"from {{ pyqt_module }} import QtWebEngineCore, QtWebEngineWidgets\"\n+{% else %}\n+  RUN python3 -c \"from {{ pyqt_module }} import QtWebKit, QtWebKitWidgets\"\n+{% endif %}\n+\n RUN useradd user -u 1001 &amp;&amp; \\\n     mkdir /home/user &amp;&amp; \\\n     chown user:users /home/user\n@@ -26,4 +72,4 @@ WORKDIR /home/user\n \n CMD git clone /outside qutebrowser.git &amp;&amp; \\\n     cd qutebrowser.git &amp;&amp; \\\n-    tox -e py\n+    tox -e {% if qt6 %}py-qt6{% else %}py-qt5{% endif %}\ndiff --git a/scripts/dev/ci/docker/generate.py b/scripts/dev/ci/docker/generate.py\nindex 2ab25f325..0f538e582 100644\n--- a/scripts/dev/ci/docker/generate.py\n+++ b/scripts/dev/ci/docker/generate.py\n@@ -1,40 +1,36 @@\n #!/usr/bin/env python3\n-# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2019-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Generate Dockerfiles for qutebrowser's CI.\"\"\"\n \n import sys\n+import argparse\n \n import jinja2\n \n \n+CONFIGS = {\n+    'archlinux-webkit': {'webengine': False, 'unstable': False, 'qt6': False},\n+    'archlinux-webengine': {'webengine': True, 'unstable': False, 'qt6': False},\n+    'archlinux-webengine-qt6': {'webengine': True, 'unstable': False, 'qt6': True},\n+    'archlinux-webengine-unstable': {'webengine': True, 'unstable': True, 'qt6': False},\n+    'archlinux-webengine-unstable-qt6': {'webengine': True, 'unstable': True, 'qt6': True},\n+}\n+\n+\n def main():\n     with open('Dockerfile.j2') as f:\n-        template = jinja2.Template(f.read())\n-\n-    image = sys.argv[1]\n-    config = {\n-        'archlinux-webkit': {'webengine': False, 'unstable': False},\n-        'archlinux-webengine': {'webengine': True, 'unstable': False},\n-        'archlinux-webengine-unstable': {'webengine': True, 'unstable': True},\n-    }[image]\n+        template = jinja2.Template(f.read(), trim_blocks=True, lstrip_blocks=True)\n+\n+    parser = argparse.ArgumentParser()\n+    parser.add_argument(\"config\", choices=CONFIGS)\n+    args = parser.parse_args()\n+\n+    config = CONFIGS[args.config]\n \n     with open('Dockerfile', 'w') as f:\n         f.write(template.render(**config))\ndiff --git a/scripts/dev/ci/problemmatchers.py b/scripts/dev/ci/problemmatchers.py\nindex c59eabeb0..3316c5597 100644\n--- a/scripts/dev/ci/problemmatchers.py\n+++ b/scripts/dev/ci/problemmatchers.py\n@@ -1,22 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Register problem matchers for GitHub Actions.\n \n@@ -173,13 +160,17 @@ MATCHERS = {\n     \"tests\": [\n         {\n             # pytest test summary output\n+            # Examples (with ANSI color codes around FAILED|ERROR and the\n+            # function name):\n+            # FAILED tests/end2end/features/test_keyinput_bdd.py::test_fakekey_sending_special_key_to_the_website - end2end.fixtures.testprocess.WaitForTimeout: Timed out after 15000ms waiting for {'category': 'js', 'message': '[*] key press: 27'}.\n+            # ERROR tests/end2end/test_insert_mode.py::test_insert_mode[100-textarea.html-qute-textarea-clipboard-qutebrowser] - Failed: Logged unexpected errors:\n             \"severity\": \"error\",\n             \"pattern\": [\n                 {\n-                    \"regexp\": r'^=+ short test summary info =+$',\n+                    \"regexp\": r'^.*=== short test summary info ===.*$',\n                 },\n                 {\n-                    \"regexp\": r\"^((ERROR|FAILED) .*)\",\n+                    \"regexp\": r\"^[^ ]*((ERROR|FAILED)[^ ]* .*)$\",\n                     \"message\": 1,\n                     \"loop\": True,\n                 }\ndiff --git a/scripts/dev/cleanup.py b/scripts/dev/cleanup.py\nindex 4f4842f3b..f53a6a5af 100755\n--- a/scripts/dev/cleanup.py\n+++ b/scripts/dev/cleanup.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Script to clean up the mess made by Python/setuptools/PyInstaller.\"\"\"\n \ndiff --git a/scripts/dev/download_release.sh b/scripts/dev/download_release.sh\ndeleted file mode 100644\nindex 207da21c8..000000000\n--- a/scripts/dev/download_release.sh\n+++ /dev/null\n@@ -1,34 +0,0 @@\n-#!/bin/bash\n-set -e\n-\n-# This script downloads the given release from GitHub so we can mirror it on\n-# qutebrowser.org.\n-\n-tmpdir=$(mktemp -d)\n-oldpwd=$PWD\n-\n-if [[ $# != 1 ]]; then\n-    echo \"Usage: $0 \" &gt;&amp;2\n-    exit 1\n-fi\n-\n-cd \"$tmpdir\"\n-mkdir windows\n-\n-base=\"https://github.com/qutebrowser/qutebrowser/releases/download/v$1\"\n-\n-wget \"$base/qutebrowser-$1.tar.gz\"\n-wget \"$base/qutebrowser-$1.tar.gz.asc\"\n-wget \"$base/qutebrowser-$1.dmg\"\n-wget \"$base/qutebrowser_${1}-1_all.deb\"\n-\n-cd windows\n-wget \"$base/qutebrowser-${1}-amd64.msi\"\n-wget \"$base/qutebrowser-${1}-win32.msi\"\n-wget \"$base/qutebrowser-${1}-windows-standalone-amd64.zip\"\n-wget \"$base/qutebrowser-${1}-windows-standalone-win32.zip\"\n-\n-dest=\"/srv/http/qutebrowser/releases/v$1\"\n-cd \"$oldpwd\"\n-sudo mv \"$tmpdir\" \"$dest\"\n-sudo chown -R http:http \"$dest\"\ndiff --git a/scripts/dev/gen_versioninfo.py b/scripts/dev/gen_versioninfo.py\nindex 56610721d..d956789c0 100644\n--- a/scripts/dev/gen_versioninfo.py\n+++ b/scripts/dev/gen_versioninfo.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Generate file_version_info.txt for Pyinstaller use with Windows builds.\"\"\"\n \ndiff --git a/scripts/dev/get_coredumpctl_traces.py b/scripts/dev/get_coredumpctl_traces.py\nindex 88a0a5b0c..c41382bf0 100644\n--- a/scripts/dev/get_coredumpctl_traces.py\n+++ b/scripts/dev/get_coredumpctl_traces.py\n@@ -1,22 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Get qutebrowser crash information and stacktraces from coredumpctl.\"\"\"\n \ndiff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py\nindex 908daad4d..4b838b5fe 100644\n--- a/scripts/dev/misc_checks.py\n+++ b/scripts/dev/misc_checks.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Various small code checkers.\"\"\"\n \n@@ -29,7 +15,7 @@ import subprocess\n import tokenize\n import traceback\n import pathlib\n-from typing import List, Iterator, Optional\n+from typing import List, Iterator, Optional, Tuple\n \n REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]\n sys.path.insert(0, str(REPO_ROOT))\n@@ -38,7 +24,7 @@ from scripts import utils\n from scripts.dev import recompile_requirements\n \n BINARY_EXTS = {'.png', '.icns', '.ico', '.bmp', '.gz', '.bin', '.pdf',\n-               '.sqlite', '.woff2', '.whl'}\n+               '.sqlite', '.woff2', '.whl', '.egg'}\n \n \n def _get_files(\n@@ -64,7 +50,7 @@ def _get_files(\n             continue\n \n         try:\n-            with tokenize.open(str(path)):\n+            with tokenize.open(path):\n                 pass\n         except SyntaxError as e:\n             # Could not find encoding\n@@ -89,11 +75,14 @@ def check_changelog_urls(_args: argparse.Namespace = None) -&gt; bool:\n         with open(outfile, 'r', encoding='utf-8') as f:\n             for line in f:\n                 line = line.strip()\n-                if line.startswith('#') or not line:\n+                if line.startswith(('#', '--')) or not line:\n                     continue\n                 req, _version = recompile_requirements.parse_versioned_line(line)\n                 if req.startswith('./'):\n                     continue\n+                if \" @ \" in req:  # vcs URL\n+                    req = req.split(\" @ \")[0]\n+\n                 all_requirements.add(req)\n                 if req not in recompile_requirements.CHANGELOG_URLS:\n                     missing.add(req)\n@@ -151,6 +140,24 @@ def _check_spelling_file(path, fobj, patterns):\n     return ok\n \n \n+def _check_spelling_all(\n+    args: argparse.Namespace,\n+    ignored: List[pathlib.Path],\n+    patterns: List[Tuple[re.Pattern, str]],\n+) -&gt; Optional[bool]:\n+    try:\n+        ok = True\n+        for path in _get_files(verbose=args.verbose, ignored=ignored):\n+            with tokenize.open(str(path)) as f:\n+                if not _check_spelling_file(path, f, patterns):\n+                    ok = False\n+        print()\n+        return ok\n+    except Exception:\n+        traceback.print_exc()\n+        return None\n+\n+\n def check_spelling(args: argparse.Namespace) -&gt; Optional[bool]:\n     \"\"\"Check commonly misspelled words.\"\"\"\n     # Words which I often misspell\n@@ -165,7 +172,7 @@ def check_spelling(args: argparse.Namespace) -&gt; Optional[bool]:\n              'artefact', 'an unix', 'an utf', 'an unicode', 'unparseable',\n              'dependancies', 'convertable', 'chosing', 'authentification'}\n \n-    # Words which look better when splitted, but might need some fine tuning.\n+    # Words which look better when split, but might need some fine tuning.\n     words |= {'webelements', 'mouseevent', 'keysequence', 'normalmode',\n               'eventloops', 'sizehint', 'statemachine', 'metaobject',\n               'logrecord'}\n@@ -259,6 +266,18 @@ def check_spelling(args: argparse.Namespace) -&gt; Optional[bool]:\n             re.compile(r'pathlib\\.Path\\(tmpdir\\)'),\n             \"use tmp_path instead\",\n         ),\n+        (\n+            re.compile(r' Copyright 2'),\n+            \"use 'SPDX-FileCopyrightText: ...' without year instead\",\n+        ),\n+        (\n+            re.compile(r'qutebrowser is free software: you can redistribute'),\n+            \"use 'SPDX-License-Identifier: GPL-3.0-or-later' instead\",\n+        ),\n+        (\n+            re.compile(r'QTimer\\(.*\\)$'),\n+            \"use usertypes.Timer() instead of a plain QTimer\",\n+        ),\n     ]\n \n     # Files which should be ignored, e.g. because they come from another\n@@ -266,22 +285,34 @@ def check_spelling(args: argparse.Namespace) -&gt; Optional[bool]:\n     hint_data = pathlib.Path('tests', 'end2end', 'data', 'hints')\n     ignored = [\n         pathlib.Path('scripts', 'dev', 'misc_checks.py'),\n+        pathlib.Path('scripts', 'dev', 'enums.txt'),\n         pathlib.Path('qutebrowser', '3rdparty', 'pdfjs'),\n+        pathlib.Path('qutebrowser', 'qt', '_core_pyqtproperty.py'),\n+        pathlib.Path('qutebrowser', 'javascript', 'caret.js'),\n         hint_data / 'ace' / 'ace.js',\n         hint_data / 'bootstrap' / 'bootstrap.css',\n     ]\n+    return _check_spelling_all(args=args, ignored=ignored, patterns=patterns)\n \n-    try:\n-        ok = True\n-        for path in _get_files(verbose=args.verbose, ignored=ignored):\n-            with tokenize.open(str(path)) as f:\n-                if not _check_spelling_file(path, f, patterns):\n-                    ok = False\n-        print()\n-        return ok\n-    except Exception:\n-        traceback.print_exc()\n-        return None\n+\n+def check_pyqt_imports(args: argparse.Namespace) -&gt; Optional[bool]:\n+    \"\"\"Check for direct PyQt imports.\"\"\"\n+    ignored = [\n+        pathlib.Path(\"qutebrowser\", \"qt\"),\n+        pathlib.Path(\"misc\", \"userscripts\"),\n+        pathlib.Path(\"scripts\"),\n+    ]\n+    patterns = [\n+        (\n+            re.compile(r\"from PyQt.* import\"),\n+            \"Use 'from qutebrowser.qt.MODULE import ...' instead\",\n+        ),\n+        (\n+            re.compile(r\"import PyQt.*\"),\n+            \"Use 'import qutebrowser.qt.MODULE' instead\",\n+        )\n+    ]\n+    return _check_spelling_all(args=args, ignored=ignored, patterns=patterns)\n \n \n def check_vcs_conflict(args: argparse.Namespace) -&gt; Optional[bool]:\n@@ -292,7 +323,7 @@ def check_vcs_conflict(args: argparse.Namespace) -&gt; Optional[bool]:\n             if path.suffix in {'.rst', '.asciidoc'}:\n                 # False positives\n                 continue\n-            with tokenize.open(str(path)) as f:\n+            with tokenize.open(path) as f:\n                 for line in f:\n                     if any(line.startswith(c * 7) for c in '&lt;&gt;=|'):\n                         print(\"Found conflict marker in {}\".format(path))\n@@ -366,14 +397,35 @@ def check_userscript_shebangs(_args: argparse.Namespace) -&gt; bool:\n     return ok\n \n \n+def check_vim_modelines(args: argparse.Namespace) -&gt; bool:\n+    \"\"\"Check that we're not using vim modelines.\"\"\"\n+    ok = True\n+    try:\n+        for path in _get_files(verbose=args.verbose):\n+            with tokenize.open(str(path)) as f:\n+                for num, line in enumerate(f, start=1):\n+                    if not line.startswith(\"# vim:\"):\n+                        continue\n+                    print(f\"{path}:{num}: Remove vim modeline \"\n+                          \"(deprecated in favor of .editorconfig)\")\n+                    ok = False\n+    except Exception:\n+        traceback.print_exc()\n+        ok = False\n+\n+    return ok\n+\n+\n def main() -&gt; int:\n     checkers = {\n         'git': check_git,\n         'vcs': check_vcs_conflict,\n         'spelling': check_spelling,\n+        'pyqt-imports': check_pyqt_imports,\n         'userscript-descriptions': check_userscripts_descriptions,\n         'userscript-shebangs': check_userscript_shebangs,\n         'changelog-urls': check_changelog_urls,\n+        'vim-modelines': check_vim_modelines,\n     }\n \n     parser = argparse.ArgumentParser()\ndiff --git a/scripts/dev/pylint_checkers/qute_pylint/config.py b/scripts/dev/pylint_checkers/qute_pylint/config.py\nindex 420ecb4ec..be5bae082 100644\n--- a/scripts/dev/pylint_checkers/qute_pylint/config.py\n+++ b/scripts/dev/pylint_checkers/qute_pylint/config.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Custom astroid checker for config calls.\"\"\"\n \n@@ -36,7 +21,6 @@ class ConfigChecker(checkers.BaseChecker):\n \n     \"\"\"Custom astroid checker for config calls.\"\"\"\n \n-    __implements__ = interfaces.IAstroidChecker\n     name = 'config'\n     msgs = {\n         'E9998': ('%s is no valid config option.',  # flake8: disable=S001\n@@ -46,7 +30,7 @@ class ConfigChecker(checkers.BaseChecker):\n     priority = -1\n     printed_warning = False\n \n-    @utils.check_messages('bad-config-option')\n+    @utils.only_required_for_messages('bad-config-option')\n     def visit_attribute(self, node):\n         \"\"\"Visit a getattr node.\"\"\"\n         # We're only interested in the end of a config.val.foo.bar chain\ndiff --git a/scripts/dev/pylint_checkers/qute_pylint/modeline.py b/scripts/dev/pylint_checkers/qute_pylint/modeline.py\ndeleted file mode 100644\nindex 1df2c375e..000000000\n--- a/scripts/dev/pylint_checkers/qute_pylint/modeline.py\n+++ /dev/null\n@@ -1,63 +0,0 @@\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n-\"\"\"Checker for vim modelines in files.\"\"\"\n-\n-import os.path\n-import contextlib\n-\n-from pylint import interfaces, checkers\n-\n-\n-class ModelineChecker(checkers.BaseChecker):\n-\n-    \"\"\"Check for vim modelines in files.\"\"\"\n-\n-    __implements__ = interfaces.IRawChecker\n-\n-    name = 'modeline'\n-    msgs = {'W9102': ('Does not have vim modeline', 'modeline-missing', None),\n-            'W9103': ('Modeline is invalid', 'invalid-modeline', None),\n-            'W9104': ('Modeline position is wrong', 'modeline-position', None)}\n-    options = ()\n-    priority = -1\n-\n-    def process_module(self, node):\n-        \"\"\"Process the module.\"\"\"\n-        if os.path.basename(os.path.splitext(node.file)[0]) == '__init__':\n-            return\n-        max_lineno = 1\n-        with contextlib.closing(node.stream()) as stream:\n-            for (lineno, line) in enumerate(stream):\n-                if lineno == 1 and line.startswith(b'#!'):\n-                    max_lineno += 1\n-                    continue\n-                elif line.startswith(b'# vim:'):\n-                    if lineno &gt; max_lineno:\n-                        self.add_message('modeline-position', line=lineno)\n-                    if (line.rstrip() != b'# vim: ft=python '\n-                                         b'fileencoding=utf-8 sts=4 sw=4 et:'):\n-                        self.add_message('invalid-modeline', line=lineno)\n-                    break\n-            else:\n-                self.add_message('modeline-missing', line=1)\n-\n-\n-def register(linter):\n-    \"\"\"Register the checker.\"\"\"\n-    linter.register_checker(ModelineChecker(linter))\ndiff --git a/scripts/dev/pylint_checkers/setup.py b/scripts/dev/pylint_checkers/setup.py\nindex e67fbe074..84768d5b9 100644\n--- a/scripts/dev/pylint_checkers/setup.py\n+++ b/scripts/dev/pylint_checkers/setup.py\n@@ -1,23 +1,8 @@\n #!/usr/bin/env python3\n \n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"This is only here so we can install those plugins in tox.ini easily.\"\"\"\n \ndiff --git a/scripts/dev/quit_segfault_test.sh b/scripts/dev/quit_segfault_test.sh\nindex 389f125b9..e8bfaed6b 100755\n--- a/scripts/dev/quit_segfault_test.sh\n+++ b/scripts/dev/quit_segfault_test.sh\n@@ -7,7 +7,7 @@ while :; do\n     exit=0\n     while (( exit == 0 )); do\n         duration=$(( RANDOM % 10000 ))\n-        python3 -m qutebrowser --debug \":later $duration quit\" http://www.heise.de/\n+        python3 -m qutebrowser --debug \":cmd-later $duration quit\" http://www.heise.de/\n         exit=$?\n     done\n     echo \"$(date) $exit $duration\" &gt;&gt; crash.log\ndiff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py\nindex 365f9a51e..838e75931 100644\n--- a/scripts/dev/recompile_requirements.py\n+++ b/scripts/dev/recompile_requirements.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Script to regenerate requirements files in misc/requirements.\"\"\"\n \n@@ -144,11 +130,11 @@ def run_pip(venv_dir, *args, quiet=False, **kwargs):\n     return subprocess.run([venv_python, '-m', 'pip'] + args, check=True, **kwargs)\n \n \n-def init_venv(host_python, venv_dir, requirements, pre=False, pip_args=None):\n+def init_venv(venv_dir, requirements, pre=False, pip_args=None):\n     \"\"\"Initialize a new virtualenv and install the given packages.\"\"\"\n     with utils.gha_group('Creating virtualenv'):\n         utils.print_col('$ python3 -m venv {}'.format(venv_dir), 'blue')\n-        subprocess.run([host_python, '-m', 'venv', venv_dir], check=True)\n+        subprocess.run([sys.executable, '-m', 'venv', venv_dir], check=True)\n \n         run_pip(venv_dir, 'install', '-U', 'pip', quiet=not utils.ON_CI)\n         run_pip(venv_dir, 'install', '-U', 'setuptools', 'wheel', quiet=not utils.ON_CI)\n@@ -347,17 +333,6 @@ def print_changed_files():\n         print('::set-output name=diff::' + diff_table)\n \n \n-def get_host_python(name):\n-    \"\"\"Get the Python to use for a given requirement name.\n-\n-    pylint installs typed_ast on &lt; 3.8 only\n-    \"\"\"\n-    if name == 'pylint':\n-        return 'python3.7'\n-    else:\n-        return sys.executable\n-\n-\n def get_venv_python(venv_dir):\n     \"\"\"Get the path to Python inside a virtualenv.\"\"\"\n     subdir = 'Scripts' if os.name == 'nt' else 'bin'\n@@ -375,14 +350,12 @@ def build_requirements(name):\n     \"\"\"Build a requirements file.\"\"\"\n     utils.print_subtitle(\"Building\")\n     filename = os.path.join(REQ_DIR, 'requirements-{}.txt-raw'.format(name))\n-    host_python = get_host_python(name)\n \n     with open(filename, 'r', encoding='utf-8') as f:\n         comments = read_comments(f)\n \n     with tempfile.TemporaryDirectory() as tmpdir:\n-        init_venv(host_python=host_python,\n-                  venv_dir=tmpdir,\n+        init_venv(venv_dir=tmpdir,\n                   requirements=filename,\n                   pre=comments['pre'],\n                   pip_args=comments['pip_args'])\n@@ -411,14 +384,13 @@ def build_requirements(name):\n \n def test_tox():\n     \"\"\"Test requirements via tox.\"\"\"\n-    host_python = get_host_python('tox')\n     req_path = os.path.join(REQ_DIR, 'requirements-tox.txt')\n \n     with tempfile.TemporaryDirectory() as tmpdir:\n         venv_dir = os.path.join(tmpdir, 'venv')\n         tox_workdir = os.path.join(tmpdir, 'tox-workdir')\n         venv_python = get_venv_python(venv_dir)\n-        init_venv(host_python, venv_dir, req_path)\n+        init_venv(venv_dir, req_path)\n         list_proc = subprocess.run([venv_python, '-m', 'tox', '--listenvs'],\n                                    check=True,\n                                    stdout=subprocess.PIPE,\n@@ -448,9 +420,8 @@ def test_requirements(name, outfile, *, force=False):\n     with open(in_file, 'r', encoding='utf-8') as f:\n         comments = read_comments(f)\n \n-    host_python = get_host_python(name)\n     with tempfile.TemporaryDirectory() as tmpdir:\n-        init_venv(host_python, tmpdir, outfile, pip_args=comments['pip_args'])\n+        init_venv(tmpdir, outfile, pip_args=comments['pip_args'])\n \n \n def cleanup_pylint_build():\ndiff --git a/scripts/dev/rewrite_enums.py b/scripts/dev/rewrite_enums.py\nindex a65c9a6e4..5d2f790a1 100644\n--- a/scripts/dev/rewrite_enums.py\n+++ b/scripts/dev/rewrite_enums.py\n@@ -1,3 +1,10 @@\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n+\"\"\"Rewrite PyQt enums based on rewrite_find_enums.py output.\"\"\"\n+\n+\n import pathlib\n import sys\n import re\n@@ -5,7 +12,7 @@ import re\n script_path = pathlib.Path(__file__).parent\n \n replacements = []\n-with (script_path / 'enums.txt').open() as f:\n+with (script_path / 'enums.txt').open(encoding=\"utf-8\") as f:\n     for line in f:\n         orig, replacement = line.split()\n         orig_re = re.compile(re.escape(orig) + r'(?=\\W)')\n@@ -16,8 +23,8 @@ for filename in sys.argv[1:]:\n     path = pathlib.Path(filename)\n     if path.suffix != '.py':\n         continue\n-    content = path.read_text()\n+    content = path.read_text(encoding=\"utf-8\")\n     print(filename)\n     for orig_re, replacement in replacements:\n         content = orig_re.sub(replacement, content)\n-    path.write_text(content)\n+    path.write_text(content, encoding=\"utf-8\")\ndiff --git a/scripts/dev/rewrite_find_enums.py b/scripts/dev/rewrite_find_enums.py\nnew file mode 100644\nindex 000000000..b4334f867\n--- /dev/null\n+++ b/scripts/dev/rewrite_find_enums.py\n@@ -0,0 +1,51 @@\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n+\"\"\"Find all PyQt enum instances.\"\"\"\n+\n+\n+import pathlib\n+import ast\n+\n+import PyQt5\n+\n+\n+def find_enums(tree):\n+    \"\"\"Find all PyQt enums in an AST tree.\"\"\"\n+    for node in ast.walk(tree):\n+        if not isinstance(node, ast.Assign):\n+            continue\n+        if node.type_comment is None:\n+            continue\n+        if '.' not in node.type_comment:\n+            continue\n+        if not node.type_comment.startswith(\"Q\"):\n+            continue\n+        comment = node.type_comment.strip(\"'\")\n+        mod, cls = comment.rsplit(\".\", maxsplit=1)\n+        assert len(node.targets) == 1\n+        name = node.targets[0].id\n+        yield (mod, cls, name)\n+\n+\n+def main():\n+    pyqt5_path = pathlib.Path(PyQt5.__file__).parent\n+    pyi_files = list(pyqt5_path.glob(\"*.pyi\"))\n+    if not pyi_files:\n+        print(\"No .pyi-files found for your PyQt installation!\")\n+    for path in pyi_files:\n+        print(f\"# {path.stem}\")\n+        tree = ast.parse(\n+            path.read_text(),\n+            filename=str(path),\n+            type_comments=True,\n+        )\n+        for mod, cls, name in find_enums(tree):\n+            old = f\"{mod}.{name}\"\n+            new = f\"{mod}.{cls}.{name}\"\n+            print(f\"{old} {new}\")\n+\n+\n+if __name__ == '__main__':\n+    main()\ndiff --git a/scripts/dev/rewrite_find_flags.py b/scripts/dev/rewrite_find_flags.py\nnew file mode 100644\nindex 000000000..110bb97e7\n--- /dev/null\n+++ b/scripts/dev/rewrite_find_flags.py\n@@ -0,0 +1,67 @@\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n+\"\"\"Find all PyQt flag instances.\"\"\"\n+\n+import pathlib\n+import ast\n+\n+import PyQt5\n+\n+\n+def find_flags(tree):\n+    \"\"\"Find all PyQt flags in an AST tree.\"\"\"\n+    for node in ast.walk(tree):\n+        if not isinstance(node, ast.FunctionDef):\n+            continue\n+        if node.name != \"__init__\":\n+            continue\n+\n+        if len(node.args.args) == 1:\n+            continue\n+\n+        annotation = node.args.args[1].annotation\n+\n+        if not isinstance(annotation, ast.Subscript):\n+            continue\n+\n+        assert isinstance(annotation.value, ast.Attribute)\n+        assert isinstance(annotation.value.value, ast.Name)\n+        assert annotation.value.value.id == \"typing\"\n+        if annotation.value.attr != \"Union\":\n+            continue\n+\n+        assert isinstance(annotation.slice, ast.Tuple)\n+        elts = annotation.slice.elts\n+\n+        if not all(isinstance(n, ast.Constant) for n in elts):\n+            continue\n+\n+        names = [n.value for n in elts]\n+        if not all(\".\" in name for name in names):\n+            continue\n+\n+        yield names\n+\n+\n+def main():\n+    pyqt5_path = pathlib.Path(PyQt5.__file__).parent\n+    pyi_files = list(pyqt5_path.glob(\"*.pyi\"))\n+    if not pyi_files:\n+        print(\"No .pyi-files found for your PyQt installation!\")\n+    for path in pyi_files:\n+        #print(f\"# {path.stem}\")\n+\n+        tree = ast.parse(\n+            path.read_text(),\n+            filename=str(path),\n+            type_comments=True,\n+        )\n+\n+        for flag, enum in find_flags(tree):\n+            print(flag, enum)\n+\n+\n+if __name__ == '__main__':\n+    main()\ndiff --git a/scripts/dev/run_profile.py b/scripts/dev/run_profile.py\nindex 2799a031b..a522b3ece 100755\n--- a/scripts/dev/run_profile.py\n+++ b/scripts/dev/run_profile.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Profile qutebrowser.\"\"\"\n \ndiff --git a/scripts/dev/run_pylint_on_tests.py b/scripts/dev/run_pylint_on_tests.py\nindex e044de976..580ef988f 100644\n--- a/scripts/dev/run_pylint_on_tests.py\n+++ b/scripts/dev/run_pylint_on_tests.py\n@@ -1,22 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Run pylint on tests.\n \n@@ -76,7 +63,7 @@ def main():\n     args = [\n         '--disable={}'.format(','.join(disabled)),\n         '--ignored-modules=helpers,pytest,PyQt5',\n-        r'--ignore-long-lines=(\n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n set -e\n \ndiff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py\nindex 1f0018488..9a8ad354f 100755\n--- a/scripts/dev/run_vulture.py\n+++ b/scripts/dev/run_vulture.py\n@@ -1,22 +1,9 @@\n #!/usr/bin/env python\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Run vulture on the source files and filter out false-positives.\"\"\"\n \n@@ -31,8 +18,8 @@ import vulture\n \n import qutebrowser.app  # pylint: disable=unused-import\n from qutebrowser.extensions import loader\n-from qutebrowser.misc import objects\n-from qutebrowser.utils import utils, version\n+from qutebrowser.misc import objects, sql, nativeeventfilter\n+from qutebrowser.utils import utils, version, qtutils\n # To run the decorators from there\n # pylint: disable=unused-import\n from qutebrowser.browser.webkit.network import webkitqutescheme\n@@ -99,8 +86,6 @@ def whitelist_generator():  # noqa: C901\n \n     for attr in ['msgs', 'priority', 'visit_attribute']:\n         yield 'scripts.dev.pylint_checkers.config.' + attr\n-    for attr in ['visit_call', 'process_module']:\n-        yield 'scripts.dev.pylint_checkers.modeline.' + attr\n \n     for name, _member in inspect.getmembers(configtypes, inspect.isclass):\n         yield 'qutebrowser.config.configtypes.' + name\n@@ -120,6 +105,9 @@ def whitelist_generator():  # noqa: C901\n     for dist in version.Distribution:\n         yield 'qutebrowser.utils.version.Distribution.{}'.format(dist.name)\n \n+    for opcode in nativeeventfilter.XcbInputOpcodes:\n+        yield f'qutebrowser.misc.nativeeventfilter.XcbInputOpcodes.{opcode.name}'\n+\n     # attrs\n     yield 'qutebrowser.browser.webkit.network.networkmanager.ProxyId.hostname'\n     yield 'qutebrowser.command.command.ArgInfo._validate_exclusive'\n@@ -139,6 +127,9 @@ def whitelist_generator():  # noqa: C901\n     yield 'ParserDictType'\n     yield 'qutebrowser.config.configutils.Values._VmapKeyType'\n \n+    # used in tests\n+    yield 'qutebrowser.qt.machinery.SelectionReason.fake'\n+\n     # ELF\n     yield 'qutebrowser.misc.elf.Endianness.big'\n     for name in ['phoff', 'ehsize', 'phentsize', 'phnum']:\n@@ -146,6 +137,13 @@ def whitelist_generator():  # noqa: C901\n     for name in ['addr', 'addralign', 'entsize']:\n         yield f'qutebrowser.misc.elf.SectionHeader.{name}'\n \n+    # For completeness\n+    for name in list(qtutils.LibraryPath):\n+        yield f'qutebrowser.utils.qtutils.LibraryPath.{name}'\n+\n+    for name in list(sql.SqliteErrorCode):\n+        yield f'qutebrowser.misc.sql.SqliteErrorCode.{name}'\n+\n \n def filter_func(item):\n     \"\"\"Check if a missing function should be filtered or not.\n@@ -178,7 +176,10 @@ def run(files):\n         whitelist_file.close()\n \n         vult = vulture.Vulture(verbose=False)\n-        vult.scavenge(files + [whitelist_file.name])\n+        vult.scavenge(\n+            files + [whitelist_file.name],\n+            exclude=[\"qutebrowser/qt/_core_pyqtproperty.py\"],\n+        )\n \n         os.remove(whitelist_file.name)\n \ndiff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py\nindex 1267a278a..ca0bdf794 100755\n--- a/scripts/dev/src2asciidoc.py\n+++ b/scripts/dev/src2asciidoc.py\n@@ -1,22 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Generate asciidoc source for qutebrowser based on docstrings.\"\"\"\n \n@@ -499,6 +486,7 @@ def _format_block(filename, what, data):\n         what: What to change (authors, options, etc.)\n         data; A list of strings which is the new data.\n     \"\"\"\n+    # pylint: disable=broad-exception-raised\n     what = what.upper()\n     oshandle, tmpname = tempfile.mkstemp()\n     try:\n@@ -525,9 +513,8 @@ def _format_block(filename, what, data):\n     except:\n         os.remove(tmpname)\n         raise\n-    else:\n-        os.remove(filename)\n-        shutil.move(tmpname, filename)\n+    os.remove(filename)\n+    shutil.move(tmpname, filename)\n \n \n def regenerate_manpage(filename):\ndiff --git a/scripts/dev/standardpaths_tester.py b/scripts/dev/standardpaths_tester.py\nindex 03de7f887..5b81f58d1 100644\n--- a/scripts/dev/standardpaths_tester.py\n+++ b/scripts/dev/standardpaths_tester.py\n@@ -1,29 +1,15 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Show various QStandardPath paths.\"\"\"\n \n import os\n import sys\n \n-from PyQt5.QtCore import (QT_VERSION_STR, PYQT_VERSION_STR, qVersion,\n+from PyQt6.QtCore import (QT_VERSION_STR, PYQT_VERSION_STR, qVersion,\n                           QStandardPaths, QCoreApplication)\n \n \ndiff --git a/scripts/dev/ua_fetch.py b/scripts/dev/ua_fetch.py\nindex 6e5bc66ac..2f094476e 100644\n--- a/scripts/dev/ua_fetch.py\n+++ b/scripts/dev/ua_fetch.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Fetch and print the most common user agents.\n \n@@ -42,6 +28,7 @@ def wrap(ini, sub, string):\n     return textwrap.wrap(string, width=80, initial_indent=ini, subsequent_indent=sub)\n \n \n+# pylint: disable-next=missing-timeout\n response = requests.get('https://raw.githubusercontent.com/Kikobeats/top-user-agents/master/index.json')\n \n if response.status_code != 200:\ndiff --git a/scripts/dev/update_3rdparty.py b/scripts/dev/update_3rdparty.py\nindex b1991fa1f..4d8a5e562 100755\n--- a/scripts/dev/update_3rdparty.py\n+++ b/scripts/dev/update_3rdparty.py\n@@ -1,23 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-# Copyright 2015 Daniel Schadt\n+# SPDX-FileCopyrightText: Daniel Schadt\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Update all third-party-modules.\"\"\"\n \n@@ -31,8 +17,6 @@ import sys\n \n sys.path.insert(\n     0, os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))\n-from scripts import dictcli\n-from qutebrowser.config import configdata\n \n \n def download_nsis_plugins():\n@@ -66,6 +50,7 @@ def download_nsis_plugins():\n \n def find_pdfjs_asset(assets, legacy):\n     \"\"\"Find the PDF.js asset to use.\"\"\"\n+    # pylint: disable=broad-exception-raised\n     for asset in assets:\n         name = asset[\"name\"]\n         if (\n@@ -82,6 +67,7 @@ def get_latest_pdfjs_url(gh_token, legacy):\n \n     Returns a (version, url)-tuple.\n     \"\"\"\n+    # pylint: disable=broad-exception-raised\n     github_api = 'https://api.github.com'\n     endpoint = 'repos/mozilla/pdf.js/releases/latest'\n     request = urllib.request.Request(f'{github_api}/{endpoint}')\n@@ -172,6 +158,8 @@ def update_ace():\n \n def test_dicts():\n     \"\"\"Test available dictionaries.\"\"\"\n+    from scripts import dictcli\n+    from qutebrowser.config import configdata\n     configdata.init()\n     for lang in dictcli.available_languages():\n         print('Testing dictionary {}... '.format(lang.code), end='')\ndiff --git a/scripts/dev/update_version.py b/scripts/dev/update_version.py\nindex 975787415..b0f48710e 100644\n--- a/scripts/dev/update_version.py\n+++ b/scripts/dev/update_version.py\n@@ -1,26 +1,14 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2018-2021 Andy Mender \n-# Copyright 2019-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Andy Mender \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Update version numbers using bump2version.\"\"\"\n \n+import re\n import sys\n import argparse\n import os.path\n@@ -32,6 +20,24 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,\n from scripts import utils\n \n \n+class Error(Exception):\n+    \"\"\"Base class for exceptions in this module.\"\"\"\n+\n+\n+def verify_branch(version_leap):\n+    \"\"\"Check that we're on the correct git branch.\"\"\"\n+    proc = subprocess.run(\n+        ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],\n+        check=True, capture_output=True, text=True)\n+    branch = proc.stdout.strip()\n+\n+    if (\n+        version_leap == 'patch' and not re.fullmatch(r'v\\d+\\.\\d+\\.x', branch) or\n+        version_leap != 'patch' and branch != 'main'\n+    ):\n+        raise Error(f\"Invalid branch for {version_leap} release: {branch}\")\n+\n+\n def bump_version(version_leap=\"patch\"):\n     \"\"\"Update qutebrowser release version.\n \n@@ -44,7 +50,11 @@ def bump_version(version_leap=\"patch\"):\n \n \n def show_commit():\n-    subprocess.run(['git', 'show'], check=True)\n+    \"\"\"Show the latest git commit.\"\"\"\n+    git_args = ['git', 'show']\n+    if utils.ON_CI:\n+        git_args.append(\"--color\")\n+    subprocess.run(git_args, check=True)\n \n \n if __name__ == \"__main__\":\n@@ -59,31 +69,39 @@ if __name__ == \"__main__\":\n     utils.change_cwd()\n \n     if not args.commands:\n+        verify_branch(args.bump)\n         bump_version(args.bump)\n         show_commit()\n \n     import qutebrowser\n     version = qutebrowser.__version__\n-    x_version = '.'.join([str(p) for p in qutebrowser.__version_info__[:-1]] +\n+    version_x = '.'.join([str(p) for p in qutebrowser.__version_info__[:-1]] +\n                          ['x'])\n \n-    print(\"Run the following commands to create a new release:\")\n-    print(\"* git push origin; git push origin v{v}\".format(v=version))\n-    if args.bump == 'patch':\n-        print(\"* git checkout master &amp;&amp; git cherry-pick v{v} &amp;&amp; \"\n-              \"git push origin\".format(v=version))\n+    if utils.ON_CI:\n+        output_file = os.environ[\"GITHUB_OUTPUT\"]\n+        with open(output_file, \"w\", encoding=\"ascii\") as f:\n+            f.write(f\"version={version}\\n\")\n+            f.write(f\"version_x={version_x}\\n\")\n+\n+        print(f\"Outputs for {version} written to GitHub Actions output file\")\n     else:\n-        print(\"* git branch v{x} v{v} &amp;&amp; git push --set-upstream origin v{x}\"\n-              .format(v=version, x=x_version))\n-    print(\"* Create new release via GitHub (required to upload release \"\n-          \"artifacts)\")\n-    print(\"* Linux: git fetch &amp;&amp; git checkout v{v} &amp;&amp; \"\n-          \"tox -e build-release -- --upload\"\n-          .format(v=version))\n-    print(\"* Windows: git fetch; git checkout v{v}; \"\n-          \"py -3.9 -m tox -e build-release -- --asciidoc \"\n-          \"$env:userprofile\\\\bin\\\\asciidoc-9.1.0\\\\asciidoc.py --upload\"\n-          .format(v=version))\n-    print(\"* macOS: git fetch &amp;&amp; git checkout v{v} &amp;&amp; \"\n-          \"tox -e build-release -- --upload\"\n-          .format(v=version))\n+        print(\"Run the following commands to create a new release:\")\n+        print(\"* git push origin; git push origin v{v}\".format(v=version))\n+        if args.bump == 'patch':\n+            print(\"* git checkout main &amp;&amp; git cherry-pick -x v{v} &amp;&amp; \"\n+                \"git push origin\".format(v=version))\n+        else:\n+            print(\"* git branch v{x} v{v} &amp;&amp; git push --set-upstream origin v{x}\"\n+                .format(v=version, x=version_x))\n+        print(\"* Create new release via GitHub (required to upload release \"\n+            \"artifacts)\")\n+        print(\"* Linux: git fetch &amp;&amp; git checkout v{v} &amp;&amp; \"\n+            \"tox -e build-release -- --upload\"\n+            .format(v=version))\n+        print(\"* Windows: git fetch; git checkout v{v}; \"\n+            \"py -3.X -m tox -e build-release -- --upload\"\n+            .format(v=version))\n+        print(\"* macOS: git fetch &amp;&amp; git checkout v{v} &amp;&amp; \"\n+            \"tox -e build-release -- --upload\"\n+            .format(v=version))\ndiff --git a/scripts/dictcli.py b/scripts/dictcli.py\nindex a937fd31d..a513c5bf7 100755\n--- a/scripts/dictcli.py\n+++ b/scripts/dictcli.py\n@@ -1,23 +1,10 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-# Copyright 2017-2018 Michal Siedlaczek \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Michal Siedlaczek \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"A script installing Hunspell dictionaries.\n \n@@ -40,7 +27,7 @@ from qutebrowser.config import configdata\n from qutebrowser.utils import standarddir\n \n \n-API_URL = 'https://chromium.googlesource.com/chromium/deps/hunspell_dictionaries.git/+/master/'\n+API_URL = 'https://chromium.googlesource.com/chromium/deps/hunspell_dictionaries.git/+/main/'\n \n \n class InvalidLanguageError(Exception):\ndiff --git a/scripts/hist_importer.py b/scripts/hist_importer.py\nindex df12bcf2e..def629961 100755\n--- a/scripts/hist_importer.py\n+++ b/scripts/hist_importer.py\n@@ -1,23 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-# Copyright 2017-2018 Josefson Souza \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Josefson Souza \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \n \"\"\"Tool to import browser history from other browsers.\"\"\"\ndiff --git a/scripts/hostblock_blame.py b/scripts/hostblock_blame.py\nindex b18c62925..7df52c9c6 100644\n--- a/scripts/hostblock_blame.py\n+++ b/scripts/hostblock_blame.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Check by which hostblock list a host was blocked.\"\"\"\n \ndiff --git a/scripts/importer.py b/scripts/importer.py\nindex d23c1e0da..cf084d178 100755\n--- a/scripts/importer.py\n+++ b/scripts/importer.py\n@@ -1,23 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-# Copyright 2014-2018 Claude (longneck) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Claude (longneck) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \n \"\"\"Tool to import data from other browsers.\n@@ -223,7 +209,7 @@ def import_html_bookmarks(bookmarks_file, bookmark_types, output_format):\n     }\n     bookmarks = []\n     for typ in bookmark_types:\n-        tags = soup.findAll(bookmark_query[typ])\n+        tags = soup.find_all(bookmark_query[typ])\n         for tag in tags:\n             if typ == 'search':\n                 tag['href'] = search_escape(tag['href']).replace('%s', '{}')\ndiff --git a/scripts/keytester.py b/scripts/keytester.py\nindex bc9a22263..7df7299c8 100644\n--- a/scripts/keytester.py\n+++ b/scripts/keytester.py\n@@ -1,30 +1,16 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Small test script to show key presses.\n \n Use python3 -m scripts.keytester to launch it.\n \"\"\"\n \n-from PyQt5.QtWidgets import QApplication\n-\n+from qutebrowser.qt.widgets import QApplication\n from qutebrowser.misc import miscwidgets\n \n app = QApplication([])\ndiff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py\nindex 0ec7f2556..108696317 100644\n--- a/scripts/link_pyqt.py\n+++ b/scripts/link_pyqt.py\n@@ -1,22 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Symlink PyQt into a given virtualenv.\"\"\"\n \n@@ -28,6 +15,7 @@ import sys\n import subprocess\n import tempfile\n import filecmp\n+import json\n \n \n class Error(Exception):\n@@ -119,31 +107,38 @@ def get_lib_path(executable, name, required=True):\n         return data\n     elif prefix == 'ImportError':\n         if required:\n-            raise Error(\"Could not import {} with {}: {}!\".format(\n-                name, executable, data))\n+            wrapper = os.environ[\"QUTE_QT_WRAPPER\"]\n+            raise Error(\n+                f\"Could not import {name} with {executable}: {data} \"\n+                f\"(QUTE_QT_WRAPPER: {wrapper})\"\n+            )\n         return None\n     else:\n         raise ValueError(\"Unexpected output: {!r}\".format(output))\n \n \n-def link_pyqt(executable, venv_path):\n+def link_pyqt(executable, venv_path, *, version):\n     \"\"\"Symlink the systemwide PyQt/sip into the venv.\n \n     Args:\n         executable: The python executable where the source files are present.\n         venv_path: The path to the virtualenv site-packages.\n+        version: The PyQt version to use.\n     \"\"\"\n+    if version not in [\"5\", \"6\"]:\n+        raise ValueError(f\"Invalid version {version}\")\n+\n     try:\n-        get_lib_path(executable, 'PyQt5.sip')\n+        get_lib_path(executable, f'PyQt{version}.sip')\n     except Error:\n-        # There is no PyQt5.sip, so we need to copy the toplevel sip.\n+        # There is no PyQt*.sip, so we need to copy the toplevel sip.\n         sip_file = get_lib_path(executable, 'sip')\n     else:\n-        # There is a PyQt5.sip, it'll get copied with the PyQt5 dir.\n+        # There is a PyQt*.sip, it'll get copied with the PyQt* dir.\n         sip_file = None\n \n     sipconfig_file = get_lib_path(executable, 'sipconfig', required=False)\n-    pyqt_dir = os.path.dirname(get_lib_path(executable, 'PyQt5.QtCore'))\n+    pyqt_dir = os.path.dirname(get_lib_path(executable, f'PyQt{version}.QtCore'))\n \n     for path in [sip_file, sipconfig_file, pyqt_dir]:\n         if path is None:\n@@ -196,9 +191,15 @@ def get_venv_lib_path(path):\n def get_tox_syspython(tox_path):\n     \"\"\"Get the system python based on a virtualenv created by tox.\"\"\"\n     path = os.path.join(tox_path, '.tox-config1')\n-    with open(path, encoding='ascii') as f:\n-        line = f.readline()\n-    _md5, sys_python = line.rstrip().split(' ', 1)\n+    if os.path.exists(path):  # tox3\n+        with open(path, encoding='ascii') as f:\n+            line = f.readline()\n+        _md5, sys_python = line.rstrip().split(' ', 1)\n+    else:  # tox4\n+        path = os.path.join(tox_path, '.tox-info.json')\n+        with open(path, encoding='utf-8') as f:\n+            data = json.load(f)\n+            sys_python = data[\"Python\"][\"executable\"]\n     # Follow symlinks to get the system-wide interpreter if we have a tox isolated\n     # build.\n     return os.path.realpath(sys_python)\n@@ -211,17 +212,11 @@ def main():\n                         action='store_true')\n     args = parser.parse_args()\n \n-    if args.tox:\n-        # Workaround for the lack of negative factors in tox.ini\n-        if 'LINK_PYQT_SKIP' in os.environ:\n-            print('LINK_PYQT_SKIP set, exiting...')\n-            sys.exit(0)\n-        executable = get_tox_syspython(args.path)\n-    else:\n-        executable = sys.executable\n+    executable = get_tox_syspython(args.path) if args.tox else sys.executable\n \n     venv_path = get_venv_lib_path(args.path)\n-    link_pyqt(executable, venv_path)\n+    wrapper = os.environ[\"QUTE_QT_WRAPPER\"]\n+    link_pyqt(executable, venv_path, version=wrapper[-1])\n \n \n if __name__ == '__main__':\ndiff --git a/scripts/mkvenv.py b/scripts/mkvenv.py\nindex 737ea145d..4ab5d8c10 100755\n--- a/scripts/mkvenv.py\n+++ b/scripts/mkvenv.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \n \"\"\"Create a local virtualenv with a PyQt install.\"\"\"\n@@ -31,13 +17,21 @@ import shutil\n import venv as pyvenv\n import subprocess\n import platform\n-from typing import List, Optional, Tuple, Dict, Union\n+from typing import List, Tuple, Dict, Union\n \n sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))\n from scripts import utils, link_pyqt\n \n \n REPO_ROOT = pathlib.Path(__file__).parent.parent\n+# for --only-binary / --no-binary\n+PYQT_PACKAGES = [\n+    \"PyQt5\",\n+    \"PyQtWebEngine\",\n+\n+    \"PyQt6\",\n+    \"PyQt6-WebEngine\",\n+]\n \n \n class Error(Exception):\n@@ -78,12 +72,12 @@ def parse_args(argv: List[str] = None) -&gt; argparse.Namespace:\n     parser.add_argument('--pyqt-wheels-dir',\n                         default='wheels',\n                         help=\"Directory to get PyQt wheels from.\")\n+    parser.add_argument('--pyqt-snapshot',\n+                        help=\"Comma-separated list to install from the Riverbank \"\n+                        \"PyQt snapshot server\")\n     parser.add_argument('--virtualenv',\n                         action='store_true',\n                         help=\"Use virtualenv instead of venv.\")\n-    parser.add_argument('--asciidoc', help=\"Full path to asciidoc.py. \"\n-                        \"If not given, asciidoc is searched in PATH.\",\n-                        nargs='?',)\n     parser.add_argument('--dev',\n                         action='store_true',\n                         help=\"Also install dev/test dependencies.\")\n@@ -108,7 +102,7 @@ def _version_key(v):\n     try:\n         return tuple(int(v) for c in v.split('.'))\n     except ValueError:\n-        return 999\n+        return (999,)\n \n \n def pyqt_versions() -&gt; List[str]:\n@@ -126,6 +120,11 @@ def pyqt_versions() -&gt; List[str]:\n     return versions + ['auto']\n \n \n+def _is_qt6_version(version: str) -&gt; bool:\n+    \"\"\"Check if the given version is Qt 6.\"\"\"\n+    return version in [\"auto\", \"6\"] or version.startswith(\"6.\")\n+\n+\n def run_venv(\n         venv_dir: pathlib.Path,\n         executable,\n@@ -180,7 +179,7 @@ def delete_old_venv(venv_dir: pathlib.Path) -&gt; None:\n                     'remove it.'.format(venv_dir))\n \n     print_command('rm -r', venv_dir, venv=False)\n-    shutil.rmtree(str(venv_dir))\n+    shutil.rmtree(venv_dir)\n \n \n def create_venv(venv_dir: pathlib.Path, use_virtualenv: bool = False) -&gt; None:\n@@ -216,7 +215,7 @@ def requirements_file(name: str) -&gt; pathlib.Path:\n \n def pyqt_requirements_file(version: str) -&gt; pathlib.Path:\n     \"\"\"Get the filename of the requirements file for the given PyQt version.\"\"\"\n-    name = 'pyqt' if version == 'auto' else 'pyqt-{}'.format(version)\n+    name = 'pyqt-6' if version == 'auto' else f'pyqt-{version}'\n     return requirements_file(name)\n \n \n@@ -226,38 +225,47 @@ def install_pyqt_binary(venv_dir: pathlib.Path, version: str) -&gt; None:\n     utils.print_col(\"No proprietary codec support will be available in \"\n                     \"qutebrowser.\", 'bold')\n \n-    supported_archs = {\n-        'linux': {'x86_64'},\n-        'win32': {'x86', 'AMD64'},\n-        'darwin': {'x86_64'},\n-    }\n+    if _is_qt6_version(version):\n+        supported_archs = {\n+            'linux': {'x86_64'},\n+            'win32': {'AMD64'},\n+            'darwin': {'x86_64', 'arm64'},\n+        }\n+    else:\n+        supported_archs = {\n+            'linux': {'x86_64'},\n+            'win32': {'x86', 'AMD64'},\n+            'darwin': {'x86_64'},\n+        }\n+\n     if sys.platform not in supported_archs:\n-        utils.print_error(f\"{sys.platform} is not a supported platform by PyQt5 binary \"\n+        utils.print_error(f\"{sys.platform} is not a supported platform by PyQt binary \"\n                           \"packages, this will most likely fail.\")\n     elif platform.machine() not in supported_archs[sys.platform]:\n         utils.print_error(\n-            f\"{platform.machine()} is not a supported architecture for PyQt5 binaries \"\n+            f\"{platform.machine()} is not a supported architecture for PyQt binaries \"\n             f\"on {sys.platform}, this will most likely fail.\")\n     elif sys.platform == 'linux' and platform.libc_ver()[0] != 'glibc':\n-        utils.print_error(\"Non-glibc Linux is not a supported platform for PyQt5 \"\n+        utils.print_error(\"Non-glibc Linux is not a supported platform for PyQt \"\n                           \"binaries, this will most likely fail.\")\n \n     pip_install(venv_dir, '-r', pyqt_requirements_file(version),\n-                '--only-binary', 'PyQt5,PyQtWebEngine')\n+                '--only-binary', ','.join(PYQT_PACKAGES))\n \n \n def install_pyqt_source(venv_dir: pathlib.Path, version: str) -&gt; None:\n     \"\"\"Install PyQt from the source tarball.\"\"\"\n     utils.print_title(\"Installing PyQt from sources\")\n     pip_install(venv_dir, '-r', pyqt_requirements_file(version),\n-                '--verbose', '--no-binary', 'PyQt5,PyQtWebEngine')\n+                '--verbose', '--no-binary', ','.join(PYQT_PACKAGES))\n \n \n-def install_pyqt_link(venv_dir: pathlib.Path) -&gt; None:\n+def install_pyqt_link(venv_dir: pathlib.Path, version: str) -&gt; None:\n     \"\"\"Install PyQt by linking a system-wide install.\"\"\"\n     utils.print_title(\"Linking system-wide PyQt\")\n     lib_path = link_pyqt.get_venv_lib_path(str(venv_dir))\n-    link_pyqt.link_pyqt(sys.executable, lib_path)\n+    major_version: str = \"6\" if _is_qt6_version(version) else \"5\"\n+    link_pyqt.link_pyqt(sys.executable, lib_path, version=major_version)\n \n \n def install_pyqt_wheels(venv_dir: pathlib.Path,\n@@ -268,6 +276,13 @@ def install_pyqt_wheels(venv_dir: pathlib.Path,\n     pip_install(venv_dir, *wheels)\n \n \n+def install_pyqt_snapshot(venv_dir: pathlib.Path, packages: List[str]) -&gt; None:\n+    \"\"\"Install PyQt packages from the snapshot server.\"\"\"\n+    utils.print_title(\"Installing PyQt snapshots\")\n+    pip_install(venv_dir, '-U', *packages, '--no-deps', '--pre',\n+                '--index-url', 'https://riverbankcomputing.com/pypi/simple/')\n+\n+\n def apply_xcb_util_workaround(\n         venv_dir: pathlib.Path,\n         pyqt_type: str,\n@@ -285,7 +300,7 @@ def apply_xcb_util_workaround(\n     if pyqt_type != 'binary':\n         print(\"Workaround not needed: Not installing from PyQt binaries.\")\n         return\n-    if pyqt_version not in ['auto', '5.15']:\n+    if _is_qt6_version(pyqt_version):\n         print(\"Workaround not needed: Not installing Qt 5.15.\")\n         return\n \n@@ -366,13 +381,17 @@ def _find_libs() -&gt; Dict[Tuple[str, str], List[str]]:\n     return all_libs\n \n \n-def run_qt_smoke_test(venv_dir: pathlib.Path) -&gt; None:\n+def run_qt_smoke_test_single(\n+    venv_dir: pathlib.Path, *,\n+    debug: bool,\n+    pyqt_version: str,\n+) -&gt; None:\n     \"\"\"Make sure the Qt installation works.\"\"\"\n     utils.print_title(\"Running Qt smoke test\")\n     code = [\n         'import sys',\n-        'from PyQt5.QtWidgets import QApplication',\n-        'from PyQt5.QtCore import qVersion, QT_VERSION_STR, PYQT_VERSION_STR',\n+        'from qutebrowser.qt.widgets import QApplication',\n+        'from qutebrowser.qt.core import qVersion, QT_VERSION_STR, PYQT_VERSION_STR',\n         'print(f\"Python: {sys.version}\")',\n         'print(f\"qVersion: {qVersion()}\")',\n         'print(f\"QT_VERSION_STR: {QT_VERSION_STR}\")',\n@@ -381,20 +400,45 @@ def run_qt_smoke_test(venv_dir: pathlib.Path) -&gt; None:\n         'print(\"Qt seems to work properly!\")',\n         'print()',\n     ]\n+    env = {\n+        'QUTE_QT_WRAPPER': 'PyQt6' if _is_qt6_version(pyqt_version) else 'PyQt5',\n+    }\n+    if debug:\n+        env['QT_DEBUG_PLUGINS'] = '1'\n+\n     try:\n         run_venv(\n             venv_dir,\n             'python', '-c', '; '.join(code),\n-            env={'QT_DEBUG_PLUGINS': '1'},\n+            env=env,\n             capture_error=True\n         )\n     except Error as e:\n         proc_e = e.__cause__\n         assert isinstance(proc_e, subprocess.CalledProcessError), proc_e\n         print(proc_e.stderr)\n-        raise Error(\n-            f\"Smoke test failed with status {proc_e.returncode}. \"\n-            \"You might find additional information in the debug output above.\")\n+\n+        msg = f\"Smoke test failed with status {proc_e.returncode}.\"\n+        if debug:\n+            msg += \" You might find additional information in the debug output above.\"\n+        raise Error(msg)\n+\n+\n+def run_qt_smoke_test(venv_dir: pathlib.Path, *, pyqt_version: str) -&gt; None:\n+    \"\"\"Make sure the Qt installation works.\"\"\"\n+    # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-104415\n+    no_debug = pyqt_version == \"6.3\" and sys.platform == \"darwin\"\n+    if no_debug:\n+        try:\n+            run_qt_smoke_test_single(venv_dir, debug=False, pyqt_version=pyqt_version)\n+        except Error as e:\n+            print(e)\n+            print(\"Rerunning with debug output...\")\n+            print(\"NOTE: This will likely segfault due to a Qt bug:\")\n+            print(\"https://bugreports.qt.io/browse/QTBUG-104415\")\n+            run_qt_smoke_test_single(venv_dir, debug=True, pyqt_version=pyqt_version)\n+    else:\n+        run_qt_smoke_test_single(venv_dir, debug=True, pyqt_version=pyqt_version)\n \n \n def install_requirements(venv_dir: pathlib.Path) -&gt; None:\n@@ -424,17 +468,14 @@ def install_qutebrowser(venv_dir: pathlib.Path) -&gt; None:\n     pip_install(venv_dir, '-e', str(REPO_ROOT))\n \n \n-def regenerate_docs(venv_dir: pathlib.Path, asciidoc: Optional[str]):\n+def regenerate_docs(venv_dir: pathlib.Path):\n     \"\"\"Regenerate docs using asciidoc.\"\"\"\n     utils.print_title(\"Generating documentation\")\n-    if asciidoc is not None:\n-        a2h_args = ['--asciidoc', asciidoc]\n-    else:\n-        a2h_args = []\n-    script_path = pathlib.Path(__file__).parent / 'asciidoc2html.py'\n+    pip_install(venv_dir, '-r', str(requirements_file('docs')))\n \n-    print_command('python3 scripts/asciidoc2html.py', *a2h_args, venv=True)\n-    run_venv(venv_dir, 'python', str(script_path), *a2h_args)\n+    script_path = pathlib.Path(__file__).parent / 'asciidoc2html.py'\n+    print_command('python3 scripts/asciidoc2html.py', venv=True)\n+    run_venv(venv_dir, 'python', str(script_path))\n \n \n def update_repo():\n@@ -450,12 +491,17 @@ def install_pyqt(venv_dir, args):\n     \"\"\"Install PyQt in the virtualenv.\"\"\"\n     if args.pyqt_type == 'binary':\n         install_pyqt_binary(venv_dir, args.pyqt_version)\n+        if args.pyqt_snapshot:\n+            install_pyqt_snapshot(venv_dir, args.pyqt_snapshot.split(','))\n     elif args.pyqt_type == 'source':\n         install_pyqt_source(venv_dir, args.pyqt_version)\n     elif args.pyqt_type == 'link':\n-        install_pyqt_link(venv_dir)\n+        install_pyqt_link(venv_dir, args.pyqt_version)\n     elif args.pyqt_type == 'wheels':\n         wheels_dir = pathlib.Path(args.pyqt_wheels_dir)\n+        if not wheels_dir.is_dir():\n+            raise Error(\n+                f\"Wheels directory {wheels_dir} doesn't exist or is not a directory\")\n         install_pyqt_wheels(venv_dir, wheels_dir)\n     elif args.pyqt_type == 'skip':\n         pass\n@@ -468,14 +514,18 @@ def run(args) -&gt; None:\n     venv_dir = pathlib.Path(args.venv_dir)\n     utils.change_cwd()\n \n-    if (args.pyqt_version != 'auto' and\n-            args.pyqt_type not in ['binary', 'source']):\n-        raise Error('The --pyqt-version option is only available when installing PyQt '\n-                    'from binary or source')\n+    if args.pyqt_version != 'auto' and args.pyqt_type == 'skip':\n+        raise Error('Cannot use --pyqt-version with --pyqt-type skip')\n+    if args.pyqt_type == 'link' and args.pyqt_version not in ['auto', '5', '6']:\n+        raise Error('Invalid --pyqt-version {args.pyqt_version}, only 5 or 6 '\n+                    'permitted with --pyqt-type=link')\n \n     if args.pyqt_wheels_dir != 'wheels' and args.pyqt_type != 'wheels':\n         raise Error('The --pyqt-wheels-dir option is only available when installing '\n                     'PyQt from wheels')\n+    if args.pyqt_snapshot and args.pyqt_type != 'binary':\n+        raise Error('The --pyqt-snapshot option is only available when installing '\n+                    'PyQt from binaries')\n \n     if args.update:\n         utils.print_title(\"Updating repository\")\n@@ -490,16 +540,16 @@ def run(args) -&gt; None:\n     install_pyqt(venv_dir, args)\n \n     apply_xcb_util_workaround(venv_dir, args.pyqt_type, args.pyqt_version)\n-    if args.pyqt_type != 'skip' and not args.skip_smoke_test:\n-        run_qt_smoke_test(venv_dir)\n \n     install_requirements(venv_dir)\n     install_qutebrowser(venv_dir)\n     if args.dev:\n         install_dev_requirements(venv_dir)\n \n+    if args.pyqt_type != 'skip' and not args.skip_smoke_test:\n+        run_qt_smoke_test(venv_dir, pyqt_version=args.pyqt_version)\n     if not args.skip_docs:\n-        regenerate_docs(venv_dir, args.asciidoc)\n+        regenerate_docs(venv_dir)\n \n \n def main():\ndiff --git a/scripts/opengl_info.py b/scripts/opengl_info.py\nindex 1bfdf22aa..7f2edb08a 100644\n--- a/scripts/opengl_info.py\n+++ b/scripts/opengl_info.py\n@@ -1,27 +1,14 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Show information about the OpenGL setup.\"\"\"\n \n-from PyQt5.QtGui import (QOpenGLContext, QOpenGLVersionProfile,\n-                         QOffscreenSurface, QGuiApplication)\n+from PyQt6.QtGui import QOpenGLContext, QOffscreenSurface, QGuiApplication\n+from PyQt6.QtOpenGL import QOpenGLVersionProfile, QOpenGLVersionFunctionsFactory\n \n app = QGuiApplication([])\n \n@@ -40,7 +27,7 @@ print(f\"GLES: {ctx.isOpenGLES()}\")\n vp = QOpenGLVersionProfile()\n vp.setVersion(2, 0)\n \n-vf = ctx.versionFunctions(vp)\n+vf = QOpenGLVersionFunctionsFactory.get(vp, ctx)\n print(f\"Vendor: {vf.glGetString(vf.GL_VENDOR)}\")\n print(f\"Renderer: {vf.glGetString(vf.GL_RENDERER)}\")\n print(f\"Version: {vf.glGetString(vf.GL_VERSION)}\")\ndiff --git a/scripts/setupcommon.py b/scripts/setupcommon.py\nindex bd549d7cc..c774a10eb 100644\n--- a/scripts/setupcommon.py\n+++ b/scripts/setupcommon.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \n \"\"\"Data used by setup.py and the PyInstaller qutebrowser.spec.\"\"\"\ndiff --git a/scripts/testbrowser/testbrowser_webengine.py b/scripts/testbrowser/testbrowser_webengine.py\nindex 73fa6828e..fbd9fa1c5 100755\n--- a/scripts/testbrowser/testbrowser_webengine.py\n+++ b/scripts/testbrowser/testbrowser_webengine.py\n@@ -1,31 +1,24 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Very simple browser for testing purposes.\"\"\"\n \n import sys\n import argparse\n \n-from PyQt5.QtCore import QUrl\n-from PyQt5.QtWidgets import QApplication\n-from PyQt5.QtWebEngineWidgets import QWebEngineView\n+try:\n+    from PyQt6.QtCore import QUrl\n+    from PyQt6.QtWidgets import QApplication\n+    from PyQt6.QtWebEngineWidgets import QWebEngineView\n+    print(\"Using PyQt6\")\n+except ImportError:\n+    from PyQt5.QtCore import QUrl\n+    from PyQt5.QtWidgets import QApplication\n+    from PyQt5.QtWebEngineWidgets import QWebEngineView\n+    print(\"Using PyQt5\")\n \n \n def parse_args():\ndiff --git a/scripts/testbrowser/testbrowser_webkit.py b/scripts/testbrowser/testbrowser_webkit.py\nindex 40938d06b..5f8f9ad46 100755\n--- a/scripts/testbrowser/testbrowser_webkit.py\n+++ b/scripts/testbrowser/testbrowser_webkit.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Very simple browser for testing purposes.\"\"\"\n \ndiff --git a/scripts/utils.py b/scripts/utils.py\nindex 6663069c9..6d3669353 100644\n--- a/scripts/utils.py\n+++ b/scripts/utils.py\n@@ -1,21 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Utility functions for scripts.\"\"\"\n \ndiff --git a/setup.py b/setup.py\nindex cb088e04c..feb949595 100755\n--- a/setup.py\n+++ b/setup.py\n@@ -1,23 +1,8 @@\n #!/usr/bin/env python3\n \n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"setuptools installer script for qutebrowser.\"\"\"\n \n@@ -66,14 +51,15 @@ def _get_constant(name):\n try:\n     common.write_git_file()\n     setuptools.setup(\n-        packages=setuptools.find_packages(exclude=['scripts', 'scripts.*']),\n+        packages=setuptools.find_namespace_packages(include=['qutebrowser',\n+                                                             'qutebrowser.*']),\n         include_package_data=True,\n         entry_points={'gui_scripts':\n                       ['qutebrowser = qutebrowser.qutebrowser:main']},\n         zip_safe=True,\n         install_requires=['jinja2', 'PyYAML',\n                           'importlib_resources&gt;=1.1.0; python_version &lt; \"3.9\"'],\n-        python_requires='&gt;=3.7',\n+        python_requires='&gt;=3.8',\n         name='qutebrowser',\n         version=_get_constant('version'),\n         description=_get_constant('description'),\n@@ -95,9 +81,10 @@ try:\n             'Operating System :: MacOS',\n             'Operating System :: POSIX :: BSD',\n             'Programming Language :: Python :: 3',\n-            'Programming Language :: Python :: 3.7',\n             'Programming Language :: Python :: 3.8',\n             'Programming Language :: Python :: 3.9',\n+            'Programming Language :: Python :: 3.10',\n+            'Programming Language :: Python :: 3.11',\n             'Topic :: Internet',\n             'Topic :: Internet :: WWW/HTTP',\n             'Topic :: Internet :: WWW/HTTP :: Browsers',\ndiff --git a/tests/conftest.py b/tests/conftest.py\nindex f7e685f4e..ddacc3db1 100644\n--- a/tests/conftest.py\n+++ b/tests/conftest.py\n@@ -1,27 +1,13 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"The qutebrowser test suite conftest file.\"\"\"\n \n import os\n import pathlib\n import sys\n+import ssl\n \n import pytest\n import hypothesis\n@@ -38,6 +24,7 @@ from helpers import testutils\n from qutebrowser.utils import usertypes, utils, version\n from qutebrowser.misc import objects, earlyinit\n \n+from qutebrowser.qt import machinery\n # To register commands\n import qutebrowser.app  # pylint: disable=unused-import\n \n@@ -57,7 +44,7 @@ hypothesis.settings.register_profile(\n         deadline=None,\n         suppress_health_check=[\n             hypothesis.HealthCheck.function_scoped_fixture,\n-            hypothesis.HealthCheck.too_slow,\n+            hypothesis.HealthCheck.too_slow\n         ]\n     )\n )\n@@ -111,6 +98,20 @@ def _apply_platform_markers(config, item):\n          pytest.mark.skipif,\n          sys.getfilesystemencoding() == 'ascii',\n          \"Skipped because of ASCII locale\"),\n+        ('qt5_only',\n+         pytest.mark.skipif,\n+         not machinery.IS_QT5,\n+         f\"Only runs on Qt 5, not {machinery.INFO.wrapper}\"),\n+        ('qt6_only',\n+         pytest.mark.skipif,\n+         not machinery.IS_QT6,\n+         f\"Only runs on Qt 6, not {machinery.INFO.wrapper}\"),\n+        ('qt5_xfail', pytest.mark.xfail, machinery.IS_QT5, \"Fails on Qt 5\"),\n+        ('qt6_xfail', pytest.mark.skipif, machinery.IS_QT6, \"Fails on Qt 6\"),\n+        ('qtwebkit_openssl3_skip',\n+         pytest.mark.skipif,\n+         not config.webengine and ssl.OPENSSL_VERSION_INFO[0] == 3,\n+         \"Failing due to cheroot: https://github.com/cherrypy/cheroot/issues/346\"),\n     ]\n \n     for searched_marker, new_marker_kind, condition, default_reason in markers:\n@@ -183,11 +184,10 @@ def pytest_collection_modifyitems(config, items):\n     items[:] = remaining_items\n \n \n-def pytest_ignore_collect(path):\n+def pytest_ignore_collect(collection_path: pathlib.Path) -&gt; bool:\n     \"\"\"Ignore BDD tests if we're unable to run them.\"\"\"\n-    fspath = pathlib.Path(path)\n     skip_bdd = hasattr(sys, 'frozen')\n-    rel_path = fspath.relative_to(pathlib.Path(__file__).parent)\n+    rel_path = collection_path.relative_to(pathlib.Path(__file__).parent)\n     return rel_path == pathlib.Path('end2end') / 'features' and skip_bdd\n \n \n@@ -196,7 +196,12 @@ def qapp_args():\n     \"\"\"Make QtWebEngine unit tests run on older Qt versions + newer kernels.\"\"\"\n     if testutils.disable_seccomp_bpf_sandbox():\n         return [sys.argv[0], testutils.DISABLE_SECCOMP_BPF_FLAG]\n-    return []\n+\n+    # Disabling PaintHoldingCrossOrigin makes tests needing UI interaction with\n+    # QtWebEngine more reliable.\n+    # Only needed with QtWebEngine and Qt 6.5, but Qt just ignores arguments it\n+    # doesn't know about anyways.\n+    return [sys.argv[0], \"--webEngineArgs\", \"--disable-features=PaintHoldingCrossOrigin\"]\n \n \n @pytest.fixture(scope='session')\n@@ -208,7 +213,9 @@ def qapp(qapp):\n \n def pytest_addoption(parser):\n     parser.addoption('--qute-delay', action='store', default=0, type=int,\n-                     help=\"Delay between qutebrowser commands.\")\n+                     help=\"Delay (in ms) between qutebrowser commands.\")\n+    parser.addoption('--qute-delay-start', action='store', default=0, type=int,\n+                     help=\"Delay (in ms) after qutebrowser process started.\")\n     parser.addoption('--qute-profile-subprocs', action='store_true',\n                      default=False, help=\"Run cProfile for subprocesses.\")\n     parser.addoption('--qute-backend', action='store',\n@@ -250,9 +257,9 @@ def _select_backend(config):\n     # Fail early if selected backend is not available\n     # pylint: disable=unused-import\n     if backend == 'webkit':\n-        import PyQt5.QtWebKitWidgets\n+        import qutebrowser.qt.webkitwidgets\n     elif backend == 'webengine':\n-        import PyQt5.QtWebEngineWidgets\n+        import qutebrowser.qt.webenginewidgets\n     else:\n         raise utils.Unreachable(backend)\n \n@@ -263,12 +270,12 @@ def _auto_select_backend():\n     # pylint: disable=unused-import\n     try:\n         # Try to use QtWebKit as the default backend\n-        import PyQt5.QtWebKitWidgets\n+        import qutebrowser.qt.webkitwidgets\n         return 'webkit'\n     except ImportError:\n         # Try to use QtWebEngine as a fallback and fail early\n         # if that's also not available\n-        import PyQt5.QtWebEngineWidgets\n+        import qutebrowser.qt.webenginewidgets\n         return 'webengine'\n \n \n@@ -284,7 +291,7 @@ def pytest_report_header(config):\n @pytest.fixture(scope='session', autouse=True)\n def check_display(request):\n     if utils.is_linux and not os.environ.get('DISPLAY', ''):\n-        raise Exception(\"No display and no Xvfb available!\")\n+        raise RuntimeError(\"No display and no Xvfb available!\")\n \n \n @pytest.fixture(autouse=True)\ndiff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py\nindex bb6ee8186..ab973175d 100644\n--- a/tests/end2end/conftest.py\n+++ b/tests/end2end/conftest.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Things needed for end2end testing.\"\"\"\n \n@@ -69,7 +54,7 @@ def _check_version(op_str, running_version, version_str, as_hex=False):\n         '&lt;': operator.lt,\n     }\n     op = operators[op_str]\n-    major, minor, patch = [int(e) for e in version_str.split('.')]\n+    major, minor, patch = (int(e) for e in version_str.split('.'))\n     if as_hex:\n         version = (major &lt;&lt; 16) | (minor &lt;&lt; 8) | patch\n     else:\n@@ -121,6 +106,7 @@ def _get_version_tag(tag):\n         try:\n             from qutebrowser.qt.webenginecore import PYQT_WEBENGINE_VERSION\n         except ImportError:\n+            # QtWebKit\n             running_version = PYQT_VERSION\n         else:\n             running_version = PYQT_WEBENGINE_VERSION\n@@ -152,7 +138,6 @@ def _get_backend_tag(tag):\n     pytest_marks = {\n         'qtwebengine_todo': pytest.mark.qtwebengine_todo,\n         'qtwebengine_skip': pytest.mark.qtwebengine_skip,\n-        'qtwebengine_notifications': pytest.mark.qtwebengine_notifications,\n         'qtwebkit_skip': pytest.mark.qtwebkit_skip,\n     }\n     if not any(tag.startswith(t + ':') for t in pytest_marks):\n@@ -179,10 +164,6 @@ if not getattr(sys, 'frozen', False):\n \n def pytest_collection_modifyitems(config, items):\n     \"\"\"Apply @qtwebengine_* markers.\"\"\"\n-    # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-75884\n-    # (note this isn't actually fixed properly before Qt 5.15)\n-    header_bug_fixed = qtutils.version_check('5.15', compiled=False)\n-\n     lib_path = pathlib.Path(QCoreApplication.libraryPaths()[0])\n     qpdf_image_plugin = lib_path / 'imageformats' / 'libqpdf.so'\n \n@@ -191,19 +172,12 @@ def pytest_collection_modifyitems(config, items):\n          config.webengine),\n         ('qtwebengine_skip', 'Skipped with QtWebEngine', pytest.mark.skipif,\n          config.webengine),\n-        ('qtwebengine_notifications',\n-         'Skipped unless QtWebEngine &gt;= 5.13',\n-         pytest.mark.skipif,\n-         not (config.webengine and qtutils.version_check('5.13'))),\n         ('qtwebkit_skip', 'Skipped with QtWebKit', pytest.mark.skipif,\n          not config.webengine),\n         ('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif,\n          config.webengine),\n         ('qtwebengine_mac_xfail', 'Fails on macOS with QtWebEngine',\n          pytest.mark.xfail, config.webengine and utils.is_mac),\n-        ('js_headers', 'Sets headers dynamically via JS',\n-         pytest.mark.skipif,\n-         config.webengine and not header_bug_fixed),\n         ('qtwebkit_pdf_imageformat_skip',\n          'Skipped with QtWebKit if PDF image plugin is available',\n          pytest.mark.skipif,\ndiff --git a/tests/end2end/data/brave-adblock/generate.py b/tests/end2end/data/brave-adblock/generate.py\nindex 393cda4e7..1dae5b6e5 100644\n--- a/tests/end2end/data/brave-adblock/generate.py\n+++ b/tests/end2end/data/brave-adblock/generate.py\n@@ -1,22 +1,8 @@\n #!/usr/bin/env python\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2020-2021 \u00c1rni Dagur \n+# SPDX-FileCopyrightText: \u00c1rni Dagur \n #\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n import io\n import gzip\ndiff --git a/tests/end2end/data/caret.html b/tests/end2end/data/caret.html\nindex 985fce0e7..2f90805c0 100644\n--- a/tests/end2end/data/caret.html\n+++ b/tests/end2end/data/caret.html\n@@ -5,7 +5,7 @@\n         Caret mode\n     \n     \n-        \none two threeeins zwei drei\n+        \none two threeeins zwei drei\n         \nfour five sixvier f\u00fcnf sechs\n     \n \ndiff --git a/tests/end2end/data/click_element.html b/tests/end2end/data/click_element.html\nindex b2a691e08..7fac2a381 100644\n--- a/tests/end2end/data/click_element.html\n+++ b/tests/end2end/data/click_element.html\n@@ -7,7 +7,7 @@\n         \"Don't\", he shouted\n         Duplicate\n         Duplicate\n-        \n\n+        \n\n         link\n         ID with dot\n         ...and how?\n         \n-        \nSee \n+        \nSee \n         here for more information.\n         \n         \nMore useless trivia!\ndiff --git a/tests/end2end/data/downloads/mhtml/complex/complex.mht b/tests/end2end/data/downloads/mhtml/complex/complex.mht\nindex a458f4dcb..7a691d6fc 100644\n--- a/tests/end2end/data/downloads/mhtml/complex/complex.mht\n+++ b/tests/end2end/data/downloads/mhtml/complex/complex.mht\n@@ -97,7 +97,7 @@ the\n         \n...and how?\n        =20\n         \nSee \n+ain/doc/contributing.asciidoc\"&gt;\n         here for more information.\n        =20\n         \nMore useless trivia!\ndiff --git a/tests/end2end/data/editor.html b/tests/end2end/data/editor.html\nindex eda6d51f0..4d4ad467a 100644\n--- a/tests/end2end/data/editor.html\n+++ b/tests/end2end/data/editor.html\n@@ -2,7 +2,7 @@\n \n     \n         \n-        Textarea\n+        Editor\n         \n             function log_text() {\n                 elem = document.getElementById(\"qute-textarea\");\n@@ -11,6 +11,7 @@\n         \n     \n     \n+        \nThis is editor.html\n         \n     \n \ndiff --git a/tests/end2end/data/hints/html/wrapped.html b/tests/end2end/data/hints/html/wrapped.html\nindex dcc05c8c7..89dceafa4 100644\n--- a/tests/end2end/data/hints/html/wrapped.html\n+++ b/tests/end2end/data/hints/html/wrapped.html\n@@ -7,7 +7,7 @@\n         \n         Link wrapped across multiple lines\n     \n-    \n+    \n         \n\n             Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n         \ndiff --git a/tests/end2end/data/hints/html/wrapped_button.html b/tests/end2end/data/hints/html/wrapped_button.html\nindex 4d5fd81bb..9ce1a28a3 100644\n--- a/tests/end2end/data/hints/html/wrapped_button.html\n+++ b/tests/end2end/data/hints/html/wrapped_button.html\n@@ -7,7 +7,7 @@\n         \n         Button link wrapped across multiple lines\n     \n-    \n+    \n         \n\n             Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n         \ndiff --git a/tests/end2end/data/hints/iframe.html b/tests/end2end/data/hints/iframe.html\nindex 269b689a2..9ecfd0b7f 100644\n--- a/tests/end2end/data/hints/iframe.html\n+++ b/tests/end2end/data/hints/iframe.html\n@@ -6,6 +6,7 @@\n         Hinting inside an iframe\n     \n     \n+        \nSome text.\n         \n     \n \ndiff --git a/tests/end2end/data/hints/iframe_button.html b/tests/end2end/data/hints/iframe_button.html\nindex 610ed4c88..c0bb0c1c7 100644\n--- a/tests/end2end/data/hints/iframe_button.html\n+++ b/tests/end2end/data/hints/iframe_button.html\n@@ -6,6 +6,7 @@\n         Hinting a button inside an iframe\n     \n     \n+        \nSome text.\n         \n     \n \ndiff --git a/tests/end2end/data/hints/iframe_scroll.html b/tests/end2end/data/hints/iframe_scroll.html\nindex 16fb0177c..af19821b3 100644\n--- a/tests/end2end/data/hints/iframe_scroll.html\n+++ b/tests/end2end/data/hints/iframe_scroll.html\n@@ -6,6 +6,7 @@\n         Scrolling inside an iframe\n     \n     \n+        \nSome text.\n         \n     \n \ndiff --git a/tests/end2end/data/hints/input.html b/tests/end2end/data/hints/input.html\nindex a81361281..c64503ada 100644\n--- a/tests/end2end/data/hints/input.html\n+++ b/tests/end2end/data/hints/input.html\n@@ -6,6 +6,7 @@\n         Simple input\n         \n           function setup_event_listener() {\n+              console.log('input loaded')\n               var elem = document.getElementById('qute-input-existing');\n               console.log(elem);\n               elem.addEventListener('input', function() {\ndiff --git a/tests/end2end/data/hints/issue3711.html b/tests/end2end/data/hints/issue3711.html\nindex 6abceccc2..51bf560c1 100644\n--- a/tests/end2end/data/hints/issue3711.html\n+++ b/tests/end2end/data/hints/issue3711.html\n@@ -3,7 +3,7 @@\n     \n         Issue 3711\n     \n-    \n+    \n         \n+\n \n \torg.qutebrowser.qutebrowser\n \ndiff --git a/tests/unit/javascript/stylesheet/test_stylesheet_js.py b/tests/unit/javascript/stylesheet/test_stylesheet_js.py\nindex 1eebe3b7f..a1d0ceaba 100644\n--- a/tests/unit/javascript/stylesheet/test_stylesheet_js.py\n+++ b/tests/unit/javascript/stylesheet/test_stylesheet_js.py\n@@ -1,29 +1,14 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Jay Kamat \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Jay Kamat \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for stylesheet.js.\"\"\"\n \n import pathlib\n import pytest\n \n-QtWebEngineWidgets = pytest.importorskip(\"PyQt5.QtWebEngineWidgets\")\n-QWebEngineProfile = QtWebEngineWidgets.QWebEngineProfile\n+QtWebEngineCore = pytest.importorskip(\"qutebrowser.qt.webenginecore\")\n+QWebEngineProfile = QtWebEngineCore.QWebEngineProfile\n \n from qutebrowser.utils import javascript\n \ndiff --git a/tests/unit/javascript/test_greasemonkey.py b/tests/unit/javascript/test_greasemonkey.py\nindex 770023c18..7d3e4d19f 100644\n--- a/tests/unit/javascript/test_greasemonkey.py\n+++ b/tests/unit/javascript/test_greasemonkey.py\n@@ -1,20 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n \"\"\"Tests for qutebrowser.browser.greasemonkey.\"\"\"\n \ndiff --git a/tests/unit/javascript/test_js_execution.py b/tests/unit/javascript/test_js_execution.py\nindex 66236a015..88a512c7d 100644\n--- a/tests/unit/javascript/test_js_execution.py\n+++ b/tests/unit/javascript/test_js_execution.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Check how Qt behaves when trying to execute JS.\"\"\"\n \n@@ -28,7 +13,7 @@ def test_simple_js_webkit(webview, js_enabled, expected):\n     \"\"\"With QtWebKit, evaluateJavaScript works when JS is on.\"\"\"\n     # If we get there (because of the webview fixture) we can be certain\n     # QtWebKit is available\n-    from qutebrowser.qt.webkit import QWebSettings\n+    from qutebrowser.qt.webkit import QWebSettings  # pylint: disable=no-name-in-module\n     webview.settings().setAttribute(QWebSettings.WebAttribute.JavascriptEnabled, js_enabled)\n     result = webview.page().mainFrame().evaluateJavaScript('1 + 1')\n     assert result == expected\n@@ -39,7 +24,7 @@ def test_element_js_webkit(webview, js_enabled, expected):\n     \"\"\"With QtWebKit, evaluateJavaScript on an element works with JS off.\"\"\"\n     # If we get there (because of the webview fixture) we can be certain\n     # QtWebKit is available\n-    from qutebrowser.qt.webkit import QWebSettings\n+    from qutebrowser.qt.webkit import QWebSettings  # pylint: disable=no-name-in-module\n     webview.settings().setAttribute(QWebSettings.WebAttribute.JavascriptEnabled, js_enabled)\n     elem = webview.page().mainFrame().documentElement()\n     result = elem.evaluateJavaScript('1 + 1')\n@@ -63,7 +48,7 @@ def test_simple_js_webengine(qtbot, webengineview, qapp,\n     \"\"\"With QtWebEngine, runJavaScript works even when JS is off.\"\"\"\n     # If we get there (because of the webengineview fixture) we can be certain\n     # QtWebEngine is available\n-    from qutebrowser.qt.webengineWidgets import QWebEngineSettings, QWebEngineScript\n+    from qutebrowser.qt.webenginecore import QWebEngineSettings, QWebEngineScript\n \n     assert world in [QWebEngineScript.ScriptWorldId.MainWorld,\n                      QWebEngineScript.ScriptWorldId.ApplicationWorld,\ndiff --git a/tests/unit/javascript/test_js_quirks.py b/tests/unit/javascript/test_js_quirks.py\nindex 52b9a090f..9218c6d0d 100644\n--- a/tests/unit/javascript/test_js_quirks.py\n+++ b/tests/unit/javascript/test_js_quirks.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for QtWebEngine JavaScript quirks.\n \n@@ -24,10 +9,14 @@ polyfills for. They should either pass because the polyfill is active, or pass b\n the native functionality exists.\n \"\"\"\n \n-import pytest\n+import pathlib\n \n+import pytest\n from qutebrowser.qt.core import QUrl\n+\n+import qutebrowser\n from qutebrowser.utils import usertypes\n+from qutebrowser.config import configdata\n \n \n @pytest.mark.parametrize('base_url, source, expected', [\n@@ -50,16 +39,10 @@ from qutebrowser.utils import usertypes\n         id='replace-all-reserved-string',\n     ),\n     pytest.param(\n-        QUrl('https://test.qutebrowser.org/test'),\n-        'typeof globalThis.setTimeout === \"function\"',\n-        True,\n-        id='global-this',\n-    ),\n-    pytest.param(\n-        QUrl(),\n-        'Object.fromEntries([[\"0\", \"a\"], [\"1\", \"b\"]])',\n-        {'0': 'a', '1': 'b'},\n-        id='object-fromentries',\n+        QUrl(\"https://test.qutebrowser.org/linkedin\"),\n+        '[1, 2, 3].at(1)',\n+        2,\n+        id='array-at',\n     ),\n ])\n def test_js_quirks(config_stub, js_tester_webengine, base_url, source, expected):\n@@ -67,3 +50,28 @@ def test_js_quirks(config_stub, js_tester_webengine, base_url, source, expected)\n     js_tester_webengine.tab._scripts._inject_site_specific_quirks()\n     js_tester_webengine.load('base.html', base_url=base_url)\n     js_tester_webengine.run(source, expected, world=usertypes.JsWorld.main)\n+\n+\n+def test_js_quirks_match_files(webengine_tab):\n+    quirks_path = pathlib.Path(qutebrowser.__file__).parent / \"javascript\" / \"quirks\"\n+    suffix = \".user.js\"\n+    quirks_files = {p.name[:-len(suffix)] for p in quirks_path.glob(f\"*{suffix}\")}\n+    quirks_code = {q.filename for q in webengine_tab._scripts._get_quirks()}\n+    assert quirks_code == quirks_files\n+\n+\n+def test_js_quirks_match_settings(webengine_tab, configdata_init):\n+    opt = configdata.DATA[\"content.site_specific_quirks.skip\"]\n+    prefix = \"js-\"\n+    valid_values = opt.typ.get_valid_values()\n+    assert valid_values is not None\n+    quirks_config = {\n+        val[len(prefix):].replace(\"-\", \"_\")\n+        for val in valid_values\n+        if val.startswith(prefix)\n+    }\n+\n+    quirks_code = {q.filename for q in webengine_tab._scripts._get_quirks()}\n+    quirks_code -= {\"googledocs\"}  # special case, UA quirk\n+\n+    assert quirks_code == quirks_config\ndiff --git a/tests/unit/keyinput/conftest.py b/tests/unit/keyinput/conftest.py\nindex cfe491df2..60bd78fb3 100644\n--- a/tests/unit/keyinput/conftest.py\n+++ b/tests/unit/keyinput/conftest.py\n@@ -1,26 +1,14 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) :\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) :\n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"pytest fixtures for tests.keyinput.\"\"\"\n \n import pytest\n \n+import contextlib\n+from qutebrowser.keyinput import keyutils\n+\n \n BINDINGS = {'prompt': {'': 'message-info ctrla',\n                        'a': 'message-info a',\n@@ -45,3 +33,20 @@ def keyinput_bindings(config_stub, key_config_stub):\n     config_stub.val.bindings.default = {}\n     config_stub.val.bindings.commands = dict(BINDINGS)\n     config_stub.val.bindings.key_mappings = dict(MAPPINGS)\n+\n+\n+@pytest.fixture\n+def pyqt_enum_workaround():\n+    \"\"\"Get a context manager to ignore invalid key errors and skip the test.\n+\n+    WORKAROUND for\n+    https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044607.html\n+    \"\"\"\n+    @contextlib.contextmanager\n+    def _pyqt_enum_workaround(exctype=keyutils.InvalidKeyError):\n+        try:\n+            yield\n+        except exctype as e:\n+            pytest.skip(f\"PyQt enum workaround: {e}\")\n+\n+    return _pyqt_enum_workaround\ndiff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py\nindex f9b31feaf..5b05252f2 100644\n--- a/tests/unit/keyinput/key_data.py\n+++ b/tests/unit/keyinput/key_data.py\n@@ -1,23 +1,8 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n-# pylint: disable=line-too-long\n+# FIXME:v4 (lint): disable=line-too-long\n \n \n \"\"\"Data used by test_keyutils.py to test all keys.\"\"\"\n@@ -26,6 +11,7 @@ import dataclasses\n from typing import Optional\n \n from qutebrowser.qt.core import Qt\n+from qutebrowser.keyinput import keyutils\n \n \n @dataclasses.dataclass(order=True)\n@@ -50,7 +36,7 @@ class Key:\n \n     def __post_init__(self):\n         if self.attribute:\n-            self.member = getattr(Qt, 'Key_' + self.attribute, None)\n+            self.member = getattr(Qt.Key, 'Key_' + self.attribute, None)\n         if self.name is None:\n             self.name = self.attribute\n \n@@ -72,7 +58,7 @@ class Modifier:\n     member: Optional[int] = None\n \n     def __post_init__(self):\n-        self.member = getattr(Qt, self.attribute + 'Modifier')\n+        self.member = getattr(Qt.KeyboardModifier, self.attribute + 'Modifier')\n         if self.name is None:\n             self.name = self.attribute\n \n@@ -450,6 +436,8 @@ KEYS = [\n     Key('LaunchD', 'Launch (D)'),\n     Key('LaunchE', 'Launch (E)'),\n     Key('LaunchF', 'Launch (F)'),\n+    Key('LaunchG', 'Launch (G)', qtest=False),\n+    Key('LaunchH', 'Launch (H)', qtest=False),\n     Key('MonBrightnessUp', 'Monitor Brightness Up', qtest=False),\n     Key('MonBrightnessDown', 'Monitor Brightness Down', qtest=False),\n     Key('KeyboardLightOnOff', 'Keyboard Light On/Off', qtest=False),\n@@ -476,7 +464,7 @@ KEYS = [\n     Key('Book', qtest=False),\n     Key('CD', qtest=False),\n     Key('Calculator', qtest=False),\n-    Key('ToDoList', 'To Do List', qtest=False),\n+    Key('ToDoList', 'To-do list', qtest=False),\n     Key('ClearGrab', 'Clear Grab', qtest=False),\n     Key('Close', qtest=False),\n     Key('Copy', qtest=False),\n@@ -541,10 +529,7 @@ KEYS = [\n     Key('TopMenu', 'Top Menu', qtest=False),\n     Key('PowerDown', 'Power Down', qtest=False),\n     Key('Suspend', qtest=False),\n-    Key('ContrastAdjust', 'Contrast Adjust', qtest=False),\n-\n-    Key('LaunchG', 'Launch (G)', qtest=False),\n-    Key('LaunchH', 'Launch (H)', qtest=False),\n+    Key('ContrastAdjust', 'Adjust contrast', qtest=False),\n \n     Key('TouchpadToggle', 'Touchpad Toggle', qtest=False),\n     Key('TouchpadOn', 'Touchpad On', qtest=False),\n@@ -609,7 +594,7 @@ KEYS = [\n \n     Key('unknown', 'Unknown', qtest=False),\n     # 0x0 is used by Qt for unknown keys...\n-    Key(attribute='', name='nil', member=0x0, qtest=False),\n+    Key(attribute='', name='nil', member=keyutils._NIL_KEY, qtest=False),\n ]\n \n \ndiff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py\nindex c2592c197..ec7c225bf 100644\n--- a/tests/unit/keyinput/test_basekeyparser.py\n+++ b/tests/unit/keyinput/test_basekeyparser.py\n@@ -1,24 +1,12 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) :\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) :\n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for BaseKeyParser.\"\"\"\n \n+import logging\n+import re\n+import sys\n from unittest import mock\n \n from qutebrowser.qt.core import Qt\n@@ -173,7 +161,7 @@ class TestHandle:\n         assert not prompt_keyparser._count\n \n     def test_invalid_key(self, prompt_keyparser):\n-        keys = [Qt.Key.Key_B, 0x0]\n+        keys = [Qt.Key.Key_B, keyutils._NIL_KEY]\n         for key in keys:\n             info = keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier)\n             prompt_keyparser.handle(info.to_event())\n@@ -357,3 +345,20 @@ def test_respect_config_when_matching_counts(keyparser, config_stub):\n \n     assert not keyparser._sequence\n     assert not keyparser._count\n+\n+\n+def test_count_limit_exceeded(handle_text, keyparser, caplog):\n+    try:\n+        max_digits = sys.get_int_max_str_digits()\n+    except AttributeError:\n+        pytest.skip('sys.get_int_max_str_digits() not available')\n+\n+    keys = (max_digits + 1) * [Qt.Key.Key_1]\n+\n+    with caplog.at_level(logging.ERROR):\n+        handle_text(keyparser, *keys, Qt.Key.Key_B, Qt.Key.Key_A)\n+\n+    pattern = re.compile(r\"^Failed to parse count: Exceeds the limit .* for integer string conversion: .*\")\n+    assert any(pattern.fullmatch(msg) for msg in caplog.messages)\n+    assert not keyparser._sequence\n+    assert not keyparser._count\ndiff --git a/tests/unit/keyinput/test_bindingtrie.py b/tests/unit/keyinput/test_bindingtrie.py\nindex b0844c980..47ad56b07 100644\n--- a/tests/unit/keyinput/test_bindingtrie.py\n+++ b/tests/unit/keyinput/test_bindingtrie.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2019-2021 Jay Kamat :\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Jay Kamat :\n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for the BindingTrie.\"\"\"\n \ndiff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py\nindex 92dfa6c3d..572456a22 100644\n--- a/tests/unit/keyinput/test_keyutils.py\n+++ b/tests/unit/keyinput/test_keyutils.py\n@@ -1,36 +1,33 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n import operator\n \n import hypothesis\n from hypothesis import strategies\n import pytest\n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import Qt, QEvent, pyqtSignal\n from qutebrowser.qt.gui import QKeyEvent, QKeySequence\n from qutebrowser.qt.widgets import QWidget\n \n+from helpers import testutils\n from unit.keyinput import key_data\n from qutebrowser.keyinput import keyutils\n from qutebrowser.utils import utils\n \n \n+pyqt_enum_workaround_skip = pytest.mark.skipif(\n+    isinstance(keyutils._NIL_KEY, int),\n+    reason=\"Can't create QKey for unknown keys with this PyQt version\"\n+)\n+try:\n+    OE_KEY = Qt.Key(ord('\u0152'))\n+except ValueError:\n+    OE_KEY = None  # affected tests skipped\n+\n+\n @pytest.fixture(params=key_data.KEYS, ids=lambda k: k.attribute)\n def qt_key(request):\n     \"\"\"Get all existing keys from key_data.py.\n@@ -61,8 +58,7 @@ def qtest_key(request):\n def test_key_data_keys():\n     \"\"\"Make sure all possible keys are in key_data.KEYS.\"\"\"\n     key_names = {name[len(\"Key_\"):]\n-                 for name, value in sorted(vars(Qt).items())\n-                 if isinstance(value, Qt.Key)}\n+                 for name in testutils.enum_members(Qt, Qt.Key)}\n     key_data_names = {key.attribute for key in sorted(key_data.KEYS)}\n     diff = key_names - key_data_names\n     assert not diff\n@@ -71,9 +67,8 @@ def test_key_data_keys():\n def test_key_data_modifiers():\n     \"\"\"Make sure all possible modifiers are in key_data.MODIFIERS.\"\"\"\n     mod_names = {name[:-len(\"Modifier\")]\n-                 for name, value in sorted(vars(Qt).items())\n-                 if isinstance(value, Qt.KeyboardModifier) and\n-                 value not in [Qt.KeyboardModifier.NoModifier, Qt.KeyboardModifier.KeyboardModifierMask]}\n+                 for name, value in testutils.enum_members(Qt, Qt.KeyboardModifier).items()\n+                 if value not in [Qt.KeyboardModifier.NoModifier, Qt.KeyboardModifier.KeyboardModifierMask]}\n     mod_data_names = {mod.attribute for mod in sorted(key_data.MODIFIERS)}\n     diff = mod_names - mod_data_names\n     assert not diff\n@@ -106,7 +101,7 @@ class TestKeyInfoText:\n \n         See key_data.py for inputs and expected values.\n         \"\"\"\n-        modifiers = Qt.KeyboardModifier.ShiftModifier if upper else Qt.KeyboardModifiers()\n+        modifiers = Qt.KeyboardModifier.ShiftModifier if upper else Qt.KeyboardModifier.NoModifier\n         info = keyutils.KeyInfo(qt_key.member, modifiers=modifiers)\n         expected = qt_key.uppertext if upper else qt_key.text\n         assert info.text() == expected\n@@ -125,8 +120,7 @@ class TestKeyInfoText:\n         with qtbot.wait_signal(key_tester.got_text):\n             qtbot.keyPress(key_tester, qtest_key.member)\n \n-        info = keyutils.KeyInfo(qtest_key.member,\n-                                modifiers=Qt.KeyboardModifiers())\n+        info = keyutils.KeyInfo(qtest_key.member)\n         assert info.text() == key_tester.text.lower()\n \n \n@@ -139,6 +133,7 @@ class TestKeyToString:\n         expected = qt_mod.name + '+'\n         assert keyutils._modifiers_to_string(qt_mod.member) == expected\n \n+    @pytest.mark.skipif(machinery.IS_QT6, reason=\"Can't delete enum members on PyQt 6\")\n     def test_missing(self, monkeypatch):\n         monkeypatch.delattr(keyutils.Qt, 'Key_AltGr')\n         # We don't want to test the key which is actually missing - we only\n@@ -158,10 +153,14 @@ class TestKeyToString:\n     (Qt.Key.Key_A,\n      Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.MetaModifier | Qt.KeyboardModifier.ShiftModifier,\n      ''),\n-    (ord('\u0152'), Qt.KeyboardModifier.NoModifier, '&lt;\u0152&gt;'),\n-    (ord('\u0152'), Qt.KeyboardModifier.ShiftModifier, ''),\n-    (ord('\u0152'), Qt.KeyboardModifier.GroupSwitchModifier, ''),\n-    (ord('\u0152'), Qt.KeyboardModifier.GroupSwitchModifier | Qt.KeyboardModifier.ShiftModifier, ''),\n+\n+    pytest.param(OE_KEY, Qt.KeyboardModifier.NoModifier, '&lt;\u0152&gt;',\n+                 marks=pyqt_enum_workaround_skip),\n+    pytest.param(OE_KEY, Qt.KeyboardModifier.ShiftModifier, '',\n+                 marks=pyqt_enum_workaround_skip),\n+    pytest.param(OE_KEY, Qt.KeyboardModifier.GroupSwitchModifier, '',\n+                 marks=pyqt_enum_workaround_skip),\n+    pytest.param(OE_KEY, Qt.KeyboardModifier.GroupSwitchModifier | Qt.KeyboardModifier.ShiftModifier, ''),\n \n     (Qt.Key.Key_Shift, Qt.KeyboardModifier.ShiftModifier, ''),\n     (Qt.Key.Key_Shift, Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier, ''),\n@@ -173,6 +172,14 @@ def test_key_info_str(key, modifiers, expected):\n     assert str(keyutils.KeyInfo(key, modifiers)) == expected\n \n \n+def test_key_info_repr():\n+    info = keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.ShiftModifier)\n+    expected = (\n+        \"\")\n+    assert repr(info) == expected\n+\n+\n @pytest.mark.parametrize('info1, info2, equal', [\n     (keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.NoModifier),\n      keyutils.KeyInfo(Qt.Key.Key_A, Qt.KeyboardModifier.NoModifier),\n@@ -193,9 +200,11 @@ def test_hash(info1, info2, equal):\n     (0xd867, Qt.KeyboardModifier.NoModifier, '\ud867\uddf6', '&lt;\ud867\uddf6&gt;'),\n     (0xd867, Qt.KeyboardModifier.ShiftModifier, '\ud867\uddf6', ''),\n ])\n-def test_surrogates(key, modifiers, text, expected):\n-    evt = QKeyEvent(QKeyEvent.KeyPress, key, modifiers, text)\n-    assert str(keyutils.KeyInfo.from_event(evt)) == expected\n+def test_surrogates(key, modifiers, text, expected, pyqt_enum_workaround):\n+    evt = QKeyEvent(QEvent.Type.KeyPress, key, modifiers, text)\n+    with pyqt_enum_workaround():\n+        info = keyutils.KeyInfo.from_event(evt)\n+    assert str(info) == expected\n \n \n @pytest.mark.parametrize('keys, expected', [\n@@ -204,16 +213,17 @@ def test_surrogates(key, modifiers, text, expected):\n     ([Qt.Key.Key_Shift, 0x29df6], '&lt;\ud867\uddf6&gt;'),\n     ([0x1f468, 0x200d, 0x1f468, 0x200d, 0x1f466], '&lt;\ud83d\udc68&gt;&lt;\u200d&gt;&lt;\ud83d\udc68&gt;&lt;\u200d&gt;&lt;\ud83d\udc66&gt;'),\n ])\n+@pyqt_enum_workaround_skip\n def test_surrogate_sequences(keys, expected):\n-    infos = [keyutils.KeyInfo(key) for key in keys]\n+    infos = [keyutils.KeyInfo(Qt.Key(key)) for key in keys]\n     seq = keyutils.KeySequence(*infos)\n     assert str(seq) == expected\n \n \n # This shouldn't happen, but if it does we should handle it well\n-def test_surrogate_error():\n+def test_surrogate_error(pyqt_enum_workaround):\n     evt = QKeyEvent(QEvent.Type.KeyPress, 0xd83e, Qt.KeyboardModifier.NoModifier, '\ud83e\udd1e\ud83c\udffb')\n-    with pytest.raises(keyutils.KeyParseError):\n+    with pytest.raises(keyutils.KeyParseError), pyqt_enum_workaround():\n         keyutils.KeyInfo.from_event(evt)\n \n \n@@ -262,11 +272,15 @@ class TestKeySequence:\n         seq = keyutils.KeySequence()\n         assert not seq\n \n-    @pytest.mark.parametrize('key', [Qt.Key.Key_unknown, -1, 0])\n+    @pytest.mark.parametrize('key', [Qt.Key.Key_unknown, keyutils._NIL_KEY])\n     def test_init_unknown(self, key):\n         with pytest.raises(keyutils.KeyParseError):\n             keyutils.KeySequence(keyutils.KeyInfo(key))\n \n+    def test_init_invalid(self):\n+        with pytest.raises(AssertionError):\n+            keyutils.KeyInfo(-1)\n+\n     def test_parse_unknown(self):\n         with pytest.raises(keyutils.KeyParseError):\n             keyutils.KeySequence.parse('\\x1f')\n@@ -577,14 +591,16 @@ def test_key_info_to_qt():\n     (Qt.Key.Key_Return, False),\n     (Qt.Key.Key_Enter, False),\n     (Qt.Key.Key_Space, False),\n-    (0x0, False),  # Used by Qt for unknown keys\n+    # Used by Qt for unknown keys\n+    pytest.param(keyutils._NIL_KEY, False, marks=pyqt_enum_workaround_skip),\n \n     (Qt.Key.Key_ydiaeresis, True),\n     (Qt.Key.Key_X, True),\n ])\n def test_is_printable(key, printable):\n     assert keyutils._is_printable(key) == printable\n-    assert keyutils.is_special(key, Qt.KeyboardModifier.NoModifier) != printable\n+    info = keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier)\n+    assert info.is_special() != printable\n \n \n @pytest.mark.parametrize('key, modifiers, special', [\n@@ -602,7 +618,7 @@ def test_is_printable(key, printable):\n     (Qt.Key.Key_Mode_switch, Qt.KeyboardModifier.GroupSwitchModifier, True),\n ])\n def test_is_special(key, modifiers, special):\n-    assert keyutils.is_special(key, modifiers) == special\n+    assert keyutils.KeyInfo(key, modifiers).is_special() == special\n \n \n @pytest.mark.parametrize('key, ismodifier', [\n@@ -611,17 +627,22 @@ def test_is_special(key, modifiers, special):\n     (Qt.Key.Key_Super_L, False),  # Modifier but not in _MODIFIER_MAP\n ])\n def test_is_modifier_key(key, ismodifier):\n-    assert keyutils.is_modifier_key(key) == ismodifier\n+    assert keyutils.KeyInfo(key).is_modifier_key() == ismodifier\n \n \n @pytest.mark.parametrize('func', [\n     keyutils._assert_plain_key,\n     keyutils._assert_plain_modifier,\n     keyutils._is_printable,\n-    keyutils.is_modifier_key,\n     keyutils._key_to_string,\n     keyutils._modifiers_to_string,\n+    keyutils.KeyInfo,\n ])\n def test_non_plain(func):\n+    comb = Qt.Key.Key_X | Qt.KeyboardModifier.ControlModifier\n+    if machinery.IS_QT6:\n+        # QKeyCombination\n+        comb = comb.toCombined()\n+\n     with pytest.raises(AssertionError):\n-        func(Qt.Key.Key_X | Qt.KeyboardModifier.ControlModifier)\n+        func(comb)\ndiff --git a/tests/unit/keyinput/test_modeman.py b/tests/unit/keyinput/test_modeman.py\nindex ee24098ff..679b3d91e 100644\n--- a/tests/unit/keyinput/test_modeman.py\n+++ b/tests/unit/keyinput/test_modeman.py\n@@ -1,25 +1,11 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n import pytest\n \n from qutebrowser.qt.core import Qt, QObject, pyqtSignal\n+from qutebrowser.qt.gui import QKeyEvent, QKeySequence\n \n from qutebrowser.utils import usertypes\n from qutebrowser.keyinput import keyutils\n@@ -37,8 +23,13 @@ class FakeKeyparser(QObject):\n         super().__init__()\n         self.passthrough = False\n \n-    def handle(self, evt, *, dry_run=False):\n-        return False\n+    def handle(\n+        self,\n+        evt: QKeyEvent,\n+        *,\n+        dry_run: bool = False,\n+    ) -&gt; QKeySequence.SequenceMatch:\n+        return QKeySequence.SequenceMatch.NoMatch\n \n \n @pytest.fixture\ndiff --git a/tests/unit/keyinput/test_modeparsers.py b/tests/unit/keyinput/test_modeparsers.py\nindex fa8918a0b..6a83d614b 100644\n--- a/tests/unit/keyinput/test_modeparsers.py\n+++ b/tests/unit/keyinput/test_modeparsers.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) :\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) :\n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for mode parsers.\"\"\"\n \n@@ -25,6 +10,7 @@ from qutebrowser.qt.gui import QKeySequence\n import pytest\n \n from qutebrowser.keyinput import modeparsers, keyutils\n+from qutebrowser.config import configexc\n \n \n @pytest.fixture\n@@ -122,31 +108,38 @@ class TestHintKeyParser:\n         ),\n     ])\n     def test_match(self, keyparser, hintmanager,\n-                   bindings, keychain, prefix, hint):\n-        keyparser.update_bindings(bindings)\n+                   bindings, keychain, prefix, hint, pyqt_enum_workaround):\n+        with pyqt_enum_workaround(keyutils.KeyParseError):\n+            keyparser.update_bindings(bindings)\n \n         seq = keyutils.KeySequence.parse(keychain)\n         assert len(seq) == 2\n \n+        # pylint: disable-next=no-member\n         match = keyparser.handle(seq[0].to_event())\n         assert match == QKeySequence.SequenceMatch.PartialMatch\n         assert hintmanager.keystr == prefix\n \n+        # pylint: disable-next=no-member\n         match = keyparser.handle(seq[1].to_event())\n         assert match == QKeySequence.SequenceMatch.ExactMatch\n         assert hintmanager.keystr == hint\n \n-    def test_match_key_mappings(self, config_stub, keyparser, hintmanager):\n-        config_stub.val.bindings.key_mappings = {'\u03b1': 'a', '\u03c3': 's'}\n+    def test_match_key_mappings(self, config_stub, keyparser, hintmanager,\n+                                pyqt_enum_workaround):\n+        with pyqt_enum_workaround(configexc.ValidationError):\n+            config_stub.val.bindings.key_mappings = {'\u03b1': 'a', '\u03c3': 's'}\n         keyparser.update_bindings(['aa', 'as'])\n \n         seq = keyutils.KeySequence.parse('\u03b1\u03c3')\n         assert len(seq) == 2\n \n+        # pylint: disable-next=no-member\n         match = keyparser.handle(seq[0].to_event())\n         assert match == QKeySequence.SequenceMatch.PartialMatch\n         assert hintmanager.keystr == 'a'\n \n+        # pylint: disable-next=no-member\n         match = keyparser.handle(seq[1].to_event())\n         assert match == QKeySequence.SequenceMatch.ExactMatch\n         assert hintmanager.keystr == 'as'\ndiff --git a/tests/unit/mainwindow/statusbar/test_backforward.py b/tests/unit/mainwindow/statusbar/test_backforward.py\nindex d3e033b34..f65bc9076 100644\n--- a/tests/unit/mainwindow/statusbar/test_backforward.py\n+++ b/tests/unit/mainwindow/statusbar/test_backforward.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Test Backforward widget.\"\"\"\n \n@@ -31,55 +16,72 @@ def backforward_widget(qtbot):\n     return widget\n \n \n+@pytest.fixture\n+def tabs(tabbed_browser_stubs):\n+    tabbed_browser = tabbed_browser_stubs[0]\n+    tabbed_browser.widget.current_index = 1\n+    return tabbed_browser\n+\n+\n @pytest.mark.parametrize('can_go_back, can_go_forward, expected_text', [\n     (False, False, ''),\n     (True, False, '[&lt;]'),\n     (False, True, '[&gt;]'),\n     (True, True, '[&lt;&gt;]'),\n ])\n-def test_backforward_widget(backforward_widget, tabbed_browser_stubs,\n-                            fake_web_tab, can_go_back, can_go_forward,\n-                            expected_text):\n+def test_widget_state(backforward_widget, tabs,\n+                      fake_web_tab, can_go_back, can_go_forward,\n+                      expected_text):\n     \"\"\"Ensure the Backforward widget shows the correct text.\"\"\"\n     tab = fake_web_tab(can_go_back=can_go_back, can_go_forward=can_go_forward)\n-    tabbed_browser = tabbed_browser_stubs[0]\n-    tabbed_browser.widget.current_index = 1\n-    tabbed_browser.widget.tabs = [tab]\n+    tabs.widget.tabs = [tab]\n     backforward_widget.enabled = True\n-    backforward_widget.on_tab_cur_url_changed(tabbed_browser)\n+    backforward_widget.on_tab_cur_url_changed(tabs)\n     assert backforward_widget.text() == expected_text\n     assert backforward_widget.isVisible() == bool(expected_text)\n \n-    # Check that the widget stays hidden if not in the statusbar\n-    backforward_widget.enabled = False\n-    backforward_widget.hide()\n-    backforward_widget.on_tab_cur_url_changed(tabbed_browser)\n-    assert backforward_widget.isHidden()\n \n-    # Check that the widget gets reset if empty.\n-    if can_go_back and can_go_forward:\n-        tab = fake_web_tab(can_go_back=False, can_go_forward=False)\n-        tabbed_browser.widget.tabs = [tab]\n-        backforward_widget.enabled = True\n-        backforward_widget.on_tab_cur_url_changed(tabbed_browser)\n-        assert backforward_widget.text() == ''\n-        assert not backforward_widget.isVisible()\n+def test_state_changes_on_tab_change(backforward_widget, tabs, fake_web_tab):\n+    \"\"\"Test we go invisible when switching to a tab without history.\"\"\"\n+    tab_with_history = fake_web_tab(can_go_back=True, can_go_forward=True)\n+    tab_without_history = fake_web_tab(can_go_back=False, can_go_forward=False)\n+    tabs.widget.tabs = [tab_with_history]\n+    backforward_widget.enabled = True\n+\n+    backforward_widget.on_tab_cur_url_changed(tabs)\n+    assert backforward_widget.isVisible()\n+\n+    tabs.widget.tabs = [tab_without_history]\n+    backforward_widget.on_tab_cur_url_changed(tabs)\n+    assert backforward_widget.text() == ''\n+    assert not backforward_widget.isVisible()\n \n \n-def test_none_tab(backforward_widget, tabbed_browser_stubs, fake_web_tab):\n+def test_none_tab(backforward_widget, tabs, fake_web_tab):\n     \"\"\"Make sure nothing crashes when passing None as tab.\"\"\"\n     tab = fake_web_tab(can_go_back=True, can_go_forward=True)\n-    tabbed_browser = tabbed_browser_stubs[0]\n-    tabbed_browser.widget.current_index = 1\n-    tabbed_browser.widget.tabs = [tab]\n+    tabs.widget.tabs = [tab]\n     backforward_widget.enabled = True\n-    backforward_widget.on_tab_cur_url_changed(tabbed_browser)\n+    backforward_widget.on_tab_cur_url_changed(tabs)\n \n     assert backforward_widget.text() == '[&lt;&gt;]'\n     assert backforward_widget.isVisible()\n \n-    tabbed_browser.widget.current_index = -1\n-    backforward_widget.on_tab_cur_url_changed(tabbed_browser)\n+    tabs.widget.current_index = -1\n+    backforward_widget.on_tab_cur_url_changed(tabs)\n \n     assert backforward_widget.text() == ''\n     assert not backforward_widget.isVisible()\n+\n+\n+def test_not_shown_when_disabled(backforward_widget, tabs, fake_web_tab):\n+    \"\"\"The widget shouldn't get shown on an event when it's disabled.\"\"\"\n+    tab = fake_web_tab(can_go_back=True, can_go_forward=True)\n+    tabs.widget.tabs = [tab]\n+\n+    backforward_widget.enabled = False\n+    backforward_widget.on_tab_cur_url_changed(tabs)\n+    assert not backforward_widget.isVisible()\n+\n+    backforward_widget.on_tab_changed(tab)\n+    assert not backforward_widget.isVisible()\ndiff --git a/tests/unit/mainwindow/statusbar/test_percentage.py b/tests/unit/mainwindow/statusbar/test_percentage.py\nindex 392fe03c1..0362a1265 100644\n--- a/tests/unit/mainwindow/statusbar/test_percentage.py\n+++ b/tests/unit/mainwindow/statusbar/test_percentage.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Test Percentage widget.\"\"\"\n \ndiff --git a/tests/unit/mainwindow/statusbar/test_progress.py b/tests/unit/mainwindow/statusbar/test_progress.py\nindex 888ed5943..e7075b4f9 100644\n--- a/tests/unit/mainwindow/statusbar/test_progress.py\n+++ b/tests/unit/mainwindow/statusbar/test_progress.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Test Progress widget.\"\"\"\n \n@@ -69,6 +53,14 @@ def test_tab_changed(fake_web_tab, progress_widget, progress, load_status,\n     assert actual == expected\n \n \n+def test_not_shown_when_disabled(progress_widget, fake_web_tab):\n+    \"\"\"The widget shouldn't get shown on an event when it's disabled.\"\"\"\n+    tab = fake_web_tab(progress=15, load_status=usertypes.LoadStatus.loading)\n+    progress_widget.enabled = False\n+    progress_widget.on_tab_changed(tab)\n+    assert not progress_widget.isVisible()\n+\n+\n def test_progress_affecting_statusbar_height(config_stub, fake_statusbar,\n                                              progress_widget):\n     \"\"\"Make sure the statusbar stays the same height when progress is shown.\ndiff --git a/tests/unit/mainwindow/statusbar/test_tabindex.py b/tests/unit/mainwindow/statusbar/test_tabindex.py\nindex b46f93efe..a2b38db9f 100644\n--- a/tests/unit/mainwindow/statusbar/test_tabindex.py\n+++ b/tests/unit/mainwindow/statusbar/test_tabindex.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Test TabIndex widget.\"\"\"\n \ndiff --git a/tests/unit/mainwindow/statusbar/test_textbase.py b/tests/unit/mainwindow/statusbar/test_textbase.py\nindex 6bceb4bc4..dfd64393d 100644\n--- a/tests/unit/mainwindow/statusbar/test_textbase.py\n+++ b/tests/unit/mainwindow/statusbar/test_textbase.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Test TextBase widget.\"\"\"\n from qutebrowser.qt.core import Qt\ndiff --git a/tests/unit/mainwindow/statusbar/test_url.py b/tests/unit/mainwindow/statusbar/test_url.py\nindex ff84c67e2..877778b78 100644\n--- a/tests/unit/mainwindow/statusbar/test_url.py\n+++ b/tests/unit/mainwindow/statusbar/test_url.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Clayton Craft (craftyguy) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Clayton Craft (craftyguy) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Test Statusbar url.\"\"\"\n \n@@ -24,7 +8,7 @@ import pytest\n \n from qutebrowser.qt.core import QUrl\n \n-from qutebrowser.utils import usertypes, urlutils\n+from qutebrowser.utils import usertypes, urlutils, qtutils\n from qutebrowser.mainwindow.statusbar import url\n \n \n@@ -37,6 +21,18 @@ def url_widget(qtbot, monkeypatch, config_stub):\n     return widget\n \n \n+qurl_idna2003 = pytest.mark.skipif(\n+    qtutils.version_check(\"6.3.0\", compiled=False),\n+    reason=\"Different result with Qt &gt;= 6.3.0: \"\n+    \"https://bugreports.qt.io/browse/QTBUG-85371\"\n+)\n+qurl_uts46 = pytest.mark.xfail(\n+    not qtutils.version_check(\"6.3.0\", compiled=False),\n+    reason=\"Different result with Qt &lt; 6.3.0: \"\n+    \"https://bugreports.qt.io/browse/QTBUG-85371\"\n+)\n+\n+\n @pytest.mark.parametrize('url_text, expected', [\n     ('http://example.com/foo/bar.html', 'http://example.com/foo/bar.html'),\n     ('http://test.gr/%CE%B1%CE%B2%CE%B3%CE%B4.txt', 'http://test.gr/\u03b1\u03b2\u03b3\u03b4.txt'),\n@@ -46,7 +42,16 @@ def url_widget(qtbot, monkeypatch, config_stub):\n     ('http://username:secret%20password@test.com', 'http://username@test.com'),\n     ('http://example.com%5b/', '(invalid URL!) http://example.com%5b/'),\n     # https://bugreports.qt.io/browse/QTBUG-60364\n-    ('http://www.xn--80ak6aa92e.com', 'http://www.xn--80ak6aa92e.com'),\n+    pytest.param(\n+        'http://www.xn--80ak6aa92e.com',\n+        'http://www.xn--80ak6aa92e.com',\n+        marks=qurl_idna2003\n+    ),\n+    pytest.param(\n+        'http://www.xn--80ak6aa92e.com',\n+        '(www.xn--80ak6aa92e.com) http://www.\u0430\u0440\u0440\u04cf\u0435.com',\n+        marks=qurl_uts46,\n+    ),\n     # IDN URL\n     ('http://www.\u00e4.com', '(www.xn--4ca.com) http://www.\u00e4.com'),\n     (None, ''),\ndiff --git a/tests/unit/mainwindow/test_messageview.py b/tests/unit/mainwindow/test_messageview.py\nindex 77d1911d0..230a1324e 100644\n--- a/tests/unit/mainwindow/test_messageview.py\n+++ b/tests/unit/mainwindow/test_messageview.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n import contextlib\n \n@@ -90,7 +75,11 @@ def test_word_wrap(view, qtbot):\n ])\n @pytest.mark.parametrize(\"replace\", [\"test\", None])\n def test_rich_text(view, qtbot, rich, higher, expected_format, replace):\n-    \"\"\"Rich text should be rendered appropriately.\"\"\"\n+    \"\"\"Rich text should be rendered appropriately.\n+\n+    This makes sure the title has been rendered as plain text by comparing the\n+    heights of the two widgets. To ensure consistent results, we disable word-wrapping.\n+    \"\"\"\n     level = usertypes.MessageLevel.info\n     text = 'with \nmarkup'\n     text2 = 'with \nmarkup 2'\n@@ -105,6 +94,7 @@ def test_rich_text(view, qtbot, rich, higher, expected_format, replace):\n     with ctx:\n         view.show_message(info1)\n         assert len(view._messages) == 1\n+        view._messages[0].setWordWrap(False)\n \n         height1 = view.sizeHint().height()\n         assert height1 &gt; 0\n@@ -112,10 +102,14 @@ def test_rich_text(view, qtbot, rich, higher, expected_format, replace):\n         assert view._messages[0].textFormat() == Qt.TextFormat.PlainText  # default\n \n     view.show_message(info2)\n-    height2 = view.sizeHint().height()\n     assert len(view._messages) == 1\n+    view._messages[0].setWordWrap(False)\n+\n+    height2 = view.sizeHint().height()\n+    assert height2 &gt; 0\n \n     assert view._messages[0].textFormat() == expected_format\n+\n     if higher:\n         assert height2 &gt; height1\n     else:\ndiff --git a/tests/unit/mainwindow/test_prompt.py b/tests/unit/mainwindow/test_prompt.py\nindex 4dbe5a41a..67403101c 100644\n--- a/tests/unit/mainwindow/test_prompt.py\n+++ b/tests/unit/mainwindow/test_prompt.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n import os\n \ndiff --git a/tests/unit/mainwindow/test_tabbedbrowser.py b/tests/unit/mainwindow/test_tabbedbrowser.py\nindex 2aa83735c..459027359 100644\n--- a/tests/unit/mainwindow/test_tabbedbrowser.py\n+++ b/tests/unit/mainwindow/test_tabbedbrowser.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n import pytest\n \ndiff --git a/tests/unit/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py\nindex db300d7a6..a249a6d1c 100644\n--- a/tests/unit/mainwindow/test_tabwidget.py\n+++ b/tests/unit/mainwindow/test_tabwidget.py\n@@ -1,30 +1,16 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-# Copyright 2015-2018 Daniel Schadt\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Daniel Schadt\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for the custom TabWidget/TabBar.\"\"\"\n \n import functools\n \n import pytest\n-from qutebrowser.qt.gui import QIcon, QPixmap\n+from unittest.mock import Mock\n \n+from qutebrowser.qt.gui import QIcon, QPixmap\n from qutebrowser.mainwindow import tabwidget\n from qutebrowser.utils import usertypes\n \n@@ -71,6 +57,38 @@ class TestTabWidget:\n             assert first_size == widget.tabBar().tabSizeHint(i)\n             assert first_size_min == widget.tabBar().minimumTabSizeHint(i)\n \n+    @pytest.fixture\n+    def paint_spy(self, monkeypatch):\n+        spy = Mock()\n+        monkeypatch.setattr(tabwidget, \"QStylePainter\", spy)\n+        return spy\n+\n+    def test_tab_text_edlided_for_narrow_tabs(self, paint_spy, widget, fake_web_tab):\n+        \"\"\"Make sure text gets elided for narrow tabs.\"\"\"\n+        widget.setMaximumWidth(100)\n+        widget.addTab(fake_web_tab(), \"one two three four\")\n+\n+        fake_paint_event = Mock()\n+        fake_paint_event.region.return_value.intersects.return_value = True\n+        widget.tabBar().paintEvent(fake_paint_event)\n+\n+        style_opt = paint_spy.return_value.drawControl.call_args_list[0][0][1]\n+        assert len(style_opt.text) &lt; len(widget.tabBar().tabText(0))\n+        assert style_opt.text.endswith(\"\u2026\")\n+        assert len(style_opt.text) &gt; len(\"\u2026\")\n+\n+    def test_tab_text_not_edlided_for_wide_tabs(self, paint_spy, widget, fake_web_tab):\n+        \"\"\"Make sure text doesn't get elided for wide tabs.\"\"\"\n+        widget.setMaximumWidth(200)\n+        widget.addTab(fake_web_tab(), \"one two three four\")\n+\n+        fake_paint_event = Mock()\n+        fake_paint_event.region.return_value.intersects.return_value = True\n+        widget.tabBar().paintEvent(fake_paint_event)\n+\n+        style_opt = paint_spy.return_value.drawControl.call_args_list[0][0][1]\n+        assert style_opt.text.endswith(widget.tabBar().tabText(0))\n+\n     @pytest.mark.parametrize(\"shrink_pinned\", [True, False])\n     @pytest.mark.parametrize(\"vertical\", [True, False])\n     def test_pinned_size(self, widget, fake_web_tab, config_stub,\ndiff --git a/tests/unit/misc/test_autoupdate.py b/tests/unit/misc/test_autoupdate.py\nindex 72b63b0c4..e34ebd395 100644\n--- a/tests/unit/misc/test_autoupdate.py\n+++ b/tests/unit/misc/test_autoupdate.py\n@@ -1,22 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-# Copyright 2015-2018 Alexander Cogneau (acogneau) :\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Alexander Cogneau (acogneau) :\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.autoupdate.\"\"\"\n \ndiff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py\nindex 35f8cfeec..fddf9e9e8 100644\n--- a/tests/unit/misc/test_checkpyver.py\n+++ b/tests/unit/misc/test_checkpyver.py\n@@ -1,20 +1,6 @@\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.checkpyver.\"\"\"\n \n@@ -28,7 +14,7 @@ import pytest\n from qutebrowser.misc import checkpyver\n \n \n-TEXT = (r\"At least Python 3.7 is required to run qutebrowser, but it's \"\n+TEXT = (r\"At least Python 3.8 is required to run qutebrowser, but it's \"\n         r\"running with \\d+\\.\\d+\\.\\d+.\")\n \n \ndiff --git a/tests/unit/misc/test_cmdhistory.py b/tests/unit/misc/test_cmdhistory.py\nindex 8ebbeb76a..1fd639987 100644\n--- a/tests/unit/misc/test_cmdhistory.py\n+++ b/tests/unit/misc/test_cmdhistory.py\n@@ -1,22 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The-Compiler) \n-# Copyright 2015-2018 Alexander Cogneau (acogneau) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Alexander Cogneau (acogneau) \n+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for misc.cmdhistory.History.\"\"\"\n \ndiff --git a/tests/unit/misc/test_crashdialog.py b/tests/unit/misc/test_crashdialog.py\nindex 3948127c2..49162370e 100644\n--- a/tests/unit/misc/test_crashdialog.py\n+++ b/tests/unit/misc/test_crashdialog.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.crashdialog.\"\"\"\n \ndiff --git a/tests/unit/misc/test_crashsignal.py b/tests/unit/misc/test_crashsignal.py\nnew file mode 100644\nindex 000000000..7019118e5\n--- /dev/null\n+++ b/tests/unit/misc/test_crashsignal.py\n@@ -0,0 +1,104 @@\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n+\"\"\"Tests for qutebrowser.misc.crashsignal.\"\"\"\n+\n+import signal\n+\n+import pytest\n+\n+from qutebrowser.config import configexc\n+from qutebrowser.qt.widgets import QApplication\n+from qutebrowser.misc import crashsignal, quitter\n+\n+\n+@pytest.fixture\n+def read_config_mock(mocker):\n+    # covers reload_config\n+    mocker.patch.object(\n+        crashsignal.standarddir,\n+        \"config_py\",\n+        return_value=\"config.py-unittest\",\n+    )\n+    return mocker.patch.object(\n+        crashsignal.configfiles,\n+        \"read_config_py\",\n+        autospec=True,\n+    )\n+\n+\n+@pytest.fixture\n+def signal_handler(qtbot, mocker, read_config_mock):\n+    \"\"\"Signal handler instance with all external methods mocked out.\"\"\"\n+    # covers init\n+    mocker.patch.object(crashsignal.sys, \"exit\", autospec=True)\n+    signal_handler = crashsignal.SignalHandler(\n+        app=mocker.Mock(spec=QApplication),\n+        quitter=mocker.Mock(spec=quitter.Quitter),\n+    )\n+\n+    return signal_handler\n+\n+\n+def test_handlers_registered(signal_handler):\n+    signal_handler.activate()\n+\n+    for sig, handler in signal_handler._handlers.items():\n+        registered = signal.signal(sig, signal.SIG_DFL)\n+        assert registered == handler\n+\n+\n+def test_handlers_deregistered(signal_handler):\n+    known_handler = lambda *_args: None\n+    for sig in signal_handler._handlers:\n+        signal.signal(sig, known_handler)\n+\n+    signal_handler.activate()\n+    signal_handler.deactivate()\n+\n+    for sig in signal_handler._handlers:\n+        registered = signal.signal(sig, signal.SIG_DFL)\n+        assert registered == known_handler\n+\n+\n+def test_interrupt_repeatedly(signal_handler):\n+    signal_handler.activate()\n+    test_signal = signal.SIGINT\n+\n+    expected_handlers = [\n+        signal_handler.interrupt,\n+        signal_handler.interrupt_forcefully,\n+        signal_handler.interrupt_really_forcefully,\n+    ]\n+\n+    # Call the SIGINT handler multiple times and make sure it calls the\n+    # expected sequence of functions.\n+    for expected in expected_handlers:\n+        registered = signal.signal(test_signal, signal.SIG_DFL)\n+        assert registered == expected\n+        expected(test_signal, None)\n+\n+\n+@pytest.mark.posix\n+def test_reload_config_call_on_hup(signal_handler, read_config_mock):\n+    signal_handler._handlers[signal.SIGHUP](None, None)\n+\n+    read_config_mock.assert_called_once_with(\"config.py-unittest\")\n+\n+\n+@pytest.mark.posix\n+def test_reload_config_displays_errors(signal_handler, read_config_mock, mocker):\n+    read_config_mock.side_effect = configexc.ConfigFileErrors(\n+        \"config.py\",\n+        [\n+            configexc.ConfigErrorDesc(\"no config.py\", ValueError(\"asdf\"))\n+        ]\n+    )\n+    message_mock = mocker.patch.object(crashsignal.message, \"error\")\n+\n+    signal_handler._handlers[signal.SIGHUP](None, None)\n+\n+    message_mock.assert_called_once_with(\n+        \"Errors occurred while reading config.py:\\n  no config.py: asdf\"\n+    )\ndiff --git a/tests/unit/misc/test_earlyinit.py b/tests/unit/misc/test_earlyinit.py\nindex e29cc0a39..80bab6281 100644\n--- a/tests/unit/misc/test_earlyinit.py\n+++ b/tests/unit/misc/test_earlyinit.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The-Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Test qutebrowser.misc.earlyinit.\"\"\"\n \ndiff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py\nindex 628ee8545..3f7edd143 100644\n--- a/tests/unit/misc/test_editor.py\n+++ b/tests/unit/misc/test_editor.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.editor.\"\"\"\n \n@@ -128,7 +113,7 @@ class TestFileHandling:\n         filename = pathlib.Path(editor._filename)\n         assert filename.exists()\n         filename.chmod(0o277)\n-        if os.access(str(filename), os.R_OK):\n+        if os.access(filename, os.R_OK):\n             # Docker container or similar\n             pytest.skip(\"File was still readable\")\n \ndiff --git a/tests/unit/misc/test_elf.py b/tests/unit/misc/test_elf.py\nindex 7d3248da2..6ae23357c 100644\n--- a/tests/unit/misc/test_elf.py\n+++ b/tests/unit/misc/test_elf.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2021 Florian Bruhin (The-Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n import io\n import struct\n@@ -24,7 +9,7 @@ import pytest\n import hypothesis\n from hypothesis import strategies as hst\n \n-from qutebrowser.misc import elf\n+from qutebrowser.misc import elf, binparsing\n from qutebrowser.utils import utils\n \n \n@@ -47,7 +32,7 @@ def test_format_sizes(fmt, expected):\n \n \n @pytest.mark.skipif(not utils.is_linux, reason=\"Needs Linux\")\n-def test_result(qapp, caplog):\n+def test_result(webengine_versions, qapp, caplog):\n     \"\"\"Test the real result of ELF parsing.\n \n     NOTE: If you're a distribution packager (or contributor) and see this test failing,\n@@ -57,9 +42,13 @@ def test_result(qapp, caplog):\n \n     If that happens, please report a bug about it!\n     \"\"\"\n-    pytest.importorskip('PyQt5.QtWebEngineCore')\n+    pytest.importorskip('qutebrowser.qt.webenginecore')\n \n     versions = elf.parse_webenginecore()\n+    if webengine_versions.webengine &gt;= utils.VersionNumber(6, 5):\n+        assert versions is None\n+        pytest.xfail(\"ELF file structure not supported\")\n+\n     assert versions is not None\n \n     # No failing mmap\n@@ -87,11 +76,52 @@ def test_result(qapp, caplog):\n         b\"QtWebEngine/5.15.9 Chrome/87.0.4280.144\\x00\",\n         elf.Versions(\"5.15.9\", \"87.0.4280.144\"),\n     ),\n+    # Piecing stuff together\n+    (\n+        (\n+            b\"\\x00QtWebEngine/6.4.0 Chrome/98.0.47Navigation to external protocol \"\n+            b\"blocked by sandb/webengine\\x00\"\n+            b\"lots-of-other-stuff\\x00\"\n+            b\"98.0.4758.90\\x0099.0.4844.84\\x00\"\n+        ),\n+        elf.Versions(\"6.4.0\", \"98.0.4758.90\"),\n+    ),\n ])\n def test_find_versions(data, expected):\n     assert elf._find_versions(data) == expected\n \n \n+@pytest.mark.parametrize(\"data, message\", [\n+    # No match at all\n+    (\n+        b\"blablabla\",\n+        \"No match in .rodata\"\n+    ),\n+    # Piecing stuff together: too short partial match\n+    (\n+        (\n+            b\"\\x00QtWebEngine/6.4.0 Chrome/98bla\\x00\"\n+            b\"lots-of-other-stuff\\x00\"\n+            b\"98.0.4758.90\\x0099.0.4844.84\\x00\"\n+        ),\n+        \"Inconclusive partial Chromium bytes\"\n+    ),\n+    # Piecing stuff together: no full match\n+    (\n+        (\n+            b\"\\x00QtWebEngine/6.4.0 Chrome/98.0.47blabla\"\n+            b\"lots-of-other-stuff\\x00\"\n+            b\"98.0.1234.56\\x00\"\n+        ),\n+        \"No match in .rodata for full version\"\n+    ),\n+])\n+def test_find_versions_invalid(data, message):\n+    with pytest.raises(binparsing.ParseError) as excinfo:\n+        elf._find_versions(data)\n+    assert str(excinfo.value) == message\n+\n+\n @hypothesis.given(data=hst.builds(\n     lambda *a: b''.join(a),\n     hst.sampled_from([b'', b'\\x7fELF', b'\\x7fELF\\x02\\x01\\x01']),\n@@ -102,5 +132,5 @@ def test_hypothesis(data):\n     fobj = io.BytesIO(data)\n     try:\n         elf._parse_from_file(fobj)\n-    except elf.ParseError as e:\n+    except binparsing.ParseError as e:\n         print(e)\ndiff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py\nindex 702a1dca8..7c4ff1a5d 100644\n--- a/tests/unit/misc/test_guiprocess.py\n+++ b/tests/unit/misc/test_guiprocess.py\n@@ -1,29 +1,15 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.guiprocess.\"\"\"\n \n import sys\n import logging\n+import signal\n \n import pytest\n-from qutebrowser.qt.core import QProcess, QUrl\n+from qutebrowser.qt.core import QProcess, QUrl, Qt\n \n from qutebrowser.misc import guiprocess\n from qutebrowser.utils import usertypes, utils, version\n@@ -32,9 +18,10 @@ from qutebrowser.qt import sip\n \n \n @pytest.fixture\n-def proc(qtbot, caplog):\n+def proc(qtbot, caplog, monkeypatch):\n     \"\"\"A fixture providing a GUIProcess and cleaning it up after the test.\"\"\"\n     p = guiprocess.GUIProcess('testprocess')\n+    monkeypatch.setattr(p._proc, 'processId', lambda: 1234)\n     yield p\n     if not sip.isdeleted(p._proc) and p._proc.state() != QProcess.ProcessState.NotRunning:\n         with caplog.at_level(logging.ERROR):\n@@ -146,7 +133,8 @@ def test_start_verbose(proc, qtbot, message_mock, py_proc):\n     assert msgs[0].level == usertypes.MessageLevel.info\n     assert msgs[1].level == usertypes.MessageLevel.info\n     assert msgs[0].text.startswith(\"Executing:\")\n-    assert msgs[1].text == \"Testprocess exited successfully.\"\n+    expected = \"Testprocess exited successfully. See :process 1234 for details.\"\n+    assert msgs[1].text == expected\n \n \n @pytest.mark.parametrize('stdout', [True, False])\n@@ -403,16 +391,15 @@ def test_failing_to_start(qtbot, proc, caplog, message_mock, monkeypatch, is_fla\n         with qtbot.wait_signal(proc.error, timeout=5000):\n             proc.start('this_does_not_exist_either', [])\n \n-    msg = message_mock.getmsg(usertypes.MessageLevel.error)\n-    assert msg.text.startswith(\n-        \"Testprocess 'this_does_not_exist_either' failed to start:\")\n+    expected_msg = (\n+        \"Testprocess 'this_does_not_exist_either' failed to start:\"\n+        \" 'this_does_not_exist_either' doesn't exist or isn't executable\"\n+    )\n+    if is_flatpak:\n+        expected_msg += \" inside the Flatpak container\"\n \n-    if not utils.is_windows:\n-        expected_msg = (\n-            \"Hint: Make sure 'this_does_not_exist_either' exists and is executable\")\n-        if is_flatpak:\n-            expected_msg += ' inside the Flatpak container'\n-        assert msg.text.endswith(expected_msg)\n+    msg = message_mock.getmsg(usertypes.MessageLevel.error)\n+    assert msg.text == expected_msg\n \n     assert not proc.outcome.running\n     assert proc.outcome.status is None\n@@ -430,7 +417,7 @@ def test_exit_unsuccessful(qtbot, proc, message_mock, py_proc, caplog):\n             proc.start(*py_proc('import sys; sys.exit(1)'))\n \n     msg = message_mock.getmsg(usertypes.MessageLevel.error)\n-    expected = \"Testprocess exited with status 1. See :process for details.\"\n+    expected = \"Testprocess exited with status 1. See :process 1234 for details.\"\n     assert msg.text == expected\n \n     assert not proc.outcome.running\n@@ -441,22 +428,50 @@ def test_exit_unsuccessful(qtbot, proc, message_mock, py_proc, caplog):\n     assert not proc.outcome.was_successful()\n \n \n-@pytest.mark.posix  # Can't seem to simulate a crash on Windows\n-def test_exit_crash(qtbot, proc, message_mock, py_proc, caplog):\n+@pytest.mark.posix  # Seems to be a normal exit on Windows\n+@pytest.mark.parametrize(\"signal, message, state_str, verbose\", [\n+    (\n+        signal.SIGSEGV,\n+        \"Testprocess crashed with status 11 (SIGSEGV).\",\n+        \"crashed\",\n+        False,\n+    ),\n+    (\n+        signal.SIGTERM,\n+        \"Testprocess terminated with status 15 (SIGTERM).\",\n+        \"terminated\",\n+        True,\n+    )\n+])\n+def test_exit_signal(\n+    qtbot,\n+    proc,\n+    message_mock,\n+    py_proc,\n+    caplog,\n+    signal,\n+    message,\n+    state_str,\n+    verbose,\n+):\n+    proc.verbose = verbose\n     with caplog.at_level(logging.ERROR):\n         with qtbot.wait_signal(proc.finished, timeout=10000):\n-            proc.start(*py_proc(\"\"\"\n+            proc.start(*py_proc(f\"\"\"\n                 import os, signal\n-                os.kill(os.getpid(), signal.SIGSEGV)\n+                os.kill(os.getpid(), signal.{signal.name})\n             \"\"\"))\n \n-    msg = message_mock.getmsg(usertypes.MessageLevel.error)\n-    assert msg.text == \"Testprocess crashed. See :process for details.\"\n+    if verbose:\n+        msg = message_mock.messages[-1]\n+    else:\n+        msg = message_mock.getmsg(usertypes.MessageLevel.error)\n+    assert msg.text == f\"{message} See :process 1234 for details.\"\n \n     assert not proc.outcome.running\n     assert proc.outcome.status == QProcess.ExitStatus.CrashExit\n-    assert str(proc.outcome) == 'Testprocess crashed.'\n-    assert proc.outcome.state_str() == 'crashed'\n+    assert str(proc.outcome) == message\n+    assert proc.outcome.state_str() == state_str\n     assert not proc.outcome.was_successful()\n \n \n@@ -472,7 +487,7 @@ def test_exit_unsuccessful_output(qtbot, proc, caplog, py_proc, stream):\n             \"\"\"))\n     assert caplog.messages[-2] == 'Process {}:\\ntest'.format(stream)\n     assert caplog.messages[-1] == (\n-        'Testprocess exited with status 1. See :process for details.')\n+        'Testprocess exited with status 1. See :process 1234 for details.')\n \n \n @pytest.mark.parametrize('stream', ['stdout', 'stderr'])\n@@ -519,6 +534,7 @@ def test_str(proc, py_proc):\n \n \n def test_cleanup(proc, py_proc, qtbot):\n+    proc._cleanup_timer.setTimerType(Qt.TimerType.CoarseTimer)\n     proc._cleanup_timer.setInterval(100)\n \n     with qtbot.wait_signal(proc._cleanup_timer.timeout):\ndiff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py\nindex 82b4bf533..79c2c7b7d 100644\n--- a/tests/unit/misc/test_ipc.py\n+++ b/tests/unit/misc/test_ipc.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.ipc.\"\"\"\n \n@@ -102,6 +87,7 @@ class FakeSocket(QObject):\n \n     readyRead = pyqtSignal()  # noqa: N815\n     disconnected = pyqtSignal()\n+    errorOccurred = pyqtSignal(QLocalSocket.LocalSocketError)  # noqa: N815\n \n     def __init__(self, *, error=QLocalSocket.LocalSocketError.UnknownSocketError, state=None,\n                  data=None, connect_successful=True, parent=None):\n@@ -110,9 +96,8 @@ class FakeSocket(QObject):\n         self._state_val = state\n         self._data = data\n         self._connect_successful = connect_successful\n-        self.error = stubs.FakeSignal('error', func=self._error)\n \n-    def _error(self):\n+    def error(self):\n         return self._error_val\n \n     def state(self):\n@@ -262,7 +247,7 @@ class TestSocketName:\n         elif utils.is_linux:\n             pass\n         else:\n-            raise Exception(\"Unexpected platform!\")\n+            raise AssertionError(\"Unexpected platform!\")\n \n \n class TestExceptions:\n@@ -270,10 +255,10 @@ class TestExceptions:\n     def test_listen_error(self, qlocalserver):\n         qlocalserver.listen(None)\n         exc = ipc.ListenError(qlocalserver)\n-        assert exc.code == 2\n+        assert exc.code == QAbstractSocket.SocketError.HostNotFoundError\n         assert exc.message == \"QLocalServer::listen: Name error\"\n         msg = (\"Error while listening to IPC server: QLocalServer::listen: \"\n-               \"Name error (error 2)\")\n+               \"Name error (HostNotFoundError)\")\n         assert str(exc) == msg\n \n         with pytest.raises(ipc.Error):\n@@ -284,7 +269,7 @@ class TestExceptions:\n         exc = ipc.SocketError(\"testing\", socket)\n         assert exc.code == QLocalSocket.LocalSocketError.ConnectionRefusedError\n         assert exc.message == \"Error string\"\n-        assert str(exc) == \"Error while testing: Error string (error 0)\"\n+        assert str(exc) == \"Error while testing: Error string (ConnectionRefusedError)\"\n \n         with pytest.raises(ipc.Error):\n             raise exc\n@@ -419,7 +404,7 @@ class TestOnError:\n         socket.setErrorString(\"Connection refused.\")\n \n         with pytest.raises(ipc.Error, match=r\"Error while handling IPC \"\n-                           r\"connection: Connection refused \\(error 0\\)\"):\n+                           r\"connection: Connection refused \\(ConnectionRefusedError\\)\"):\n             ipc_server.on_error(QLocalSocket.LocalSocketError.ConnectionRefusedError)\n \n \n@@ -454,7 +439,7 @@ class TestHandleConnection:\n         ipc_server._server = FakeServer(socket)\n \n         with pytest.raises(ipc.Error, match=r\"Error while handling IPC \"\n-                           r\"connection: Error string \\(error 7\\)\"):\n+                           r\"connection: Error string \\(ConnectionError\\)\"):\n             ipc_server.handle_connection()\n \n         assert \"We got an error immediately.\" in caplog.messages\n@@ -552,7 +537,8 @@ class TestSendToRunningInstance:\n     def test_no_server(self, caplog):\n         sent = ipc.send_to_running_instance('qute-test', [], None)\n         assert not sent\n-        assert caplog.messages[-1] == \"No existing instance present (error 2)\"\n+        expected = \"No existing instance present (ServerNotFoundError)\"\n+        assert caplog.messages[-1] == expected\n \n     @pytest.mark.parametrize('has_cwd', [True, False])\n     @pytest.mark.linux(reason=\"Causes random trouble on Windows and macOS\")\n@@ -590,7 +576,7 @@ class TestSendToRunningInstance:\n     def test_socket_error(self):\n         socket = FakeSocket(error=QLocalSocket.LocalSocketError.ConnectionError)\n         with pytest.raises(ipc.Error, match=r\"Error while writing to running \"\n-                           r\"instance: Error string \\(error 7\\)\"):\n+                           r\"instance: Error string \\(ConnectionError\\)\"):\n             ipc.send_to_running_instance('qute-test', [], None, socket=socket)\n \n     def test_not_disconnected_immediately(self):\n@@ -601,7 +587,7 @@ class TestSendToRunningInstance:\n         socket = FakeSocket(error=QLocalSocket.LocalSocketError.ConnectionError,\n                             connect_successful=False)\n         with pytest.raises(ipc.Error, match=r\"Error while connecting to \"\n-                           r\"running instance: Error string \\(error 7\\)\"):\n+                           r\"running instance: Error string \\(ConnectionError\\)\"):\n             ipc.send_to_running_instance('qute-test', [], None, socket=socket)\n \n \n@@ -657,6 +643,7 @@ class TestSendOrListen:\n     def qlocalserver_mock(self, mocker):\n         m = mocker.patch('qutebrowser.misc.ipc.QLocalServer', autospec=True)\n         m().errorString.return_value = \"Error string\"\n+        m.SocketOption = QLocalServer.SocketOption\n         m().newConnection = stubs.FakeSignal()\n         return m\n \n@@ -664,10 +651,8 @@ class TestSendOrListen:\n     def qlocalsocket_mock(self, mocker):\n         m = mocker.patch('qutebrowser.misc.ipc.QLocalSocket', autospec=True)\n         m().errorString.return_value = \"Error string\"\n-        for name in ['UnknownSocketError', 'UnconnectedState',\n-                     'ConnectionRefusedError', 'ServerNotFoundError',\n-                     'PeerClosedError']:\n-            setattr(m, name, getattr(QLocalSocket, name))\n+        m.LocalSocketError = QLocalSocket.LocalSocketError\n+        m.LocalSocketState = QLocalSocket.LocalSocketState\n         return m\n \n     @pytest.mark.linux(reason=\"Flaky on Windows and macOS\")\n@@ -717,9 +702,9 @@ class TestSendOrListen:\n \n     @pytest.mark.parametrize('has_error, exc_name, exc_msg', [\n         (True, 'SocketError',\n-         'Error while writing to running instance: Error string (error 0)'),\n+         'Error while writing to running instance: Error string (ConnectionRefusedError)'),\n         (False, 'AddressInUseError',\n-         'Error while listening to IPC server: Error string (error 8)'),\n+         'Error while listening to IPC server: Error string (AddressInUseError)'),\n     ])\n     def test_address_in_use_error(self, qlocalserver_mock, qlocalsocket_mock,\n                                   stubs, caplog, args, has_error, exc_name,\n@@ -746,6 +731,9 @@ class TestSendOrListen:\n             QLocalSocket.LocalSocketError.ConnectionRefusedError,\n             QLocalSocket.LocalSocketError.ConnectionRefusedError,  # error() gets called twice\n         ]\n+        # For debug.qenum_key() on Qt 5\n+        value_to_key = qlocalsocket_mock.staticMetaObject.enumerator().valueToKey\n+        value_to_key.return_value = \"ConnectionRefusedError\"\n \n         with caplog.at_level(logging.ERROR):\n             with pytest.raises(ipc.Error):\n@@ -780,7 +768,7 @@ class TestSendOrListen:\n             'pre_text: ',\n             'post_text: ',\n             ('exception text: Error while listening to IPC server: Error '\n-             'string (error 4)'),\n+             'string (SocketResourceError)'),\n         ]\n         assert caplog.messages[-1] == '\\n'.join(error_msgs)\n \ndiff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py\nindex 922f39331..e6b2669d2 100644\n--- a/tests/unit/misc/test_keyhints.py\n+++ b/tests/unit/misc/test_keyhints.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Ryan Roden-Corrent (rcorre) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Test the keyhint widget.\"\"\"\n \ndiff --git a/tests/unit/misc/test_lineparser.py b/tests/unit/misc/test_lineparser.py\nindex 0384d3498..20f20809d 100644\n--- a/tests/unit/misc/test_lineparser.py\n+++ b/tests/unit/misc/test_lineparser.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.lineparser.\"\"\"\n \ndiff --git a/tests/unit/misc/test_miscwidgets.py b/tests/unit/misc/test_miscwidgets.py\nindex b6c0873dd..3fd2c2f93 100644\n--- a/tests/unit/misc/test_miscwidgets.py\n+++ b/tests/unit/misc/test_miscwidgets.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n import logging\n \n@@ -110,6 +95,21 @@ class TestWrapperLayout:\n \n class TestFullscreenNotification:\n \n+    @pytest.fixture\n+    def widget_factory(self, qtbot, key_config_stub, blue_widget):\n+        # We need to pass a visible parent widget, so that\n+        # FullscreenNotification.__init__ can access\n+        # self.window().windowHandle() to get the screen.\n+        with qtbot.wait_exposed(blue_widget):\n+            blue_widget.show()\n+\n+        def create():\n+            w = miscwidgets.FullscreenNotification(blue_widget)\n+            qtbot.add_widget(w)\n+            return w\n+\n+        return create\n+\n     @pytest.mark.parametrize('bindings, text', [\n         ({'': 'fullscreen --leave'},\n          \"Press  to exit fullscreen.\"),\n@@ -117,18 +117,16 @@ class TestFullscreenNotification:\n         ({'a': 'fullscreen --leave'}, \"Press a to exit fullscreen.\"),\n         ({}, \"Page is now fullscreen.\"),\n     ])\n-    def test_text(self, qtbot, config_stub, key_config_stub, bindings, text):\n+    def test_text(self, widget_factory, config_stub, bindings, text):\n         config_stub.val.bindings.default = {}\n         config_stub.val.bindings.commands = {'normal': bindings}\n-        w = miscwidgets.FullscreenNotification()\n-        qtbot.add_widget(w)\n-        assert w.text() == text\n-\n-    def test_timeout(self, qtbot, key_config_stub):\n-        w = miscwidgets.FullscreenNotification()\n-        qtbot.add_widget(w)\n-        with qtbot.wait_signal(w.destroyed):\n-            w.set_timeout(1)\n+        widget = widget_factory()\n+        assert widget.text() == text\n+\n+    def test_timeout(self, qtbot, widget_factory):\n+        widget = widget_factory()\n+        with qtbot.wait_signal(widget.destroyed):\n+            widget.set_timeout(1)\n \n \n @pytest.mark.usefixtures('state_config')\ndiff --git a/tests/unit/misc/test_msgbox.py b/tests/unit/misc/test_msgbox.py\nindex 6eee28f32..29a1d6e68 100644\n--- a/tests/unit/misc/test_msgbox.py\n+++ b/tests/unit/misc/test_msgbox.py\n@@ -1,20 +1,6 @@\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.msgbox.\"\"\"\n \ndiff --git a/tests/unit/misc/test_objects.py b/tests/unit/misc/test_objects.py\nindex 44425eb29..124b98216 100644\n--- a/tests/unit/misc/test_objects.py\n+++ b/tests/unit/misc/test_objects.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n import pytest\n \ndiff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py\nnew file mode 100644\nindex 000000000..59185a380\n--- /dev/null\n+++ b/tests/unit/misc/test_pakjoy.py\n@@ -0,0 +1,453 @@\n+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n+import os\n+import io\n+import json\n+import struct\n+import pathlib\n+import logging\n+import shutil\n+\n+import pytest\n+\n+from qutebrowser.misc import pakjoy, binparsing\n+from qutebrowser.utils import utils, version, standarddir\n+\n+\n+pytest.importorskip(\"qutebrowser.qt.webenginecore\")\n+\n+\n+pytestmark = pytest.mark.usefixtures(\"cache_tmpdir\")\n+\n+\n+versions = version.qtwebengine_versions(avoid_init=True)\n+\n+\n+# Used to skip happy path tests with the real resources file.\n+#\n+# Since we don't know how reliably the Google Meet hangouts extensions is\n+# reliably in the resource files, and this quirk is only targeting 6.6\n+# anyway.\n+skip_if_unsupported = pytest.mark.skipif(\n+    versions.webengine != utils.VersionNumber(6, 6),\n+    reason=\"Code under test only runs on 6.6\",\n+)\n+\n+\n+@pytest.fixture(autouse=True)\n+def prepare_env(qapp, monkeypatch):\n+    monkeypatch.setattr(pakjoy.objects, \"qapp\", qapp)\n+    monkeypatch.delenv(pakjoy.RESOURCES_ENV_VAR, raising=False)\n+    monkeypatch.delenv(pakjoy.DISABLE_ENV_VAR, raising=False)\n+\n+\n+def patch_version(monkeypatch: pytest.MonkeyPatch, qtwe_version: utils.VersionNumber):\n+    monkeypatch.setattr(\n+        pakjoy.version,\n+        \"qtwebengine_versions\",\n+        lambda **kwargs: version.WebEngineVersions(\n+            webengine=qtwe_version,\n+            chromium=None,\n+            source=\"unittest\",\n+        ),\n+    )\n+\n+\n+@pytest.fixture(params=[\n+    utils.VersionNumber(6, 4),\n+    utils.VersionNumber(6, 5, 3),\n+    utils.VersionNumber(6, 6, 1),\n+    utils.VersionNumber(6, 7),\n+])\n+def unaffected_version(monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest, config_stub):\n+    config_stub.val.colors.webpage.darkmode.enabled = True\n+    patch_version(monkeypatch, request.param)\n+\n+\n+@pytest.fixture(params=[\n+    utils.VersionNumber(6, 5),\n+    utils.VersionNumber(6, 5, 1),\n+    utils.VersionNumber(6, 5, 2),\n+    utils.VersionNumber(6, 6),\n+])\n+def affected_version(monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest, config_stub):\n+    config_stub.val.colors.webpage.darkmode.enabled = True\n+    patch_version(monkeypatch, request.param)\n+\n+\n+@pytest.mark.parametrize(\"workdir_exists\", [True, False])\n+def test_version_gate(cache_tmpdir, unaffected_version, mocker, workdir_exists):\n+    workdir = cache_tmpdir / pakjoy.CACHE_DIR_NAME\n+    if workdir_exists:\n+        workdir.mkdir()\n+        (workdir / \"some_patched_file.pak\").ensure()\n+    fake_open = mocker.patch(\"qutebrowser.misc.pakjoy.open\")\n+\n+    with pakjoy.patch_webengine():\n+        pass\n+\n+    assert not fake_open.called\n+    assert not workdir.exists()\n+\n+\n+def test_escape_hatch(affected_version, mocker, monkeypatch):\n+    fake_open = mocker.patch(\"qutebrowser.misc.pakjoy.open\")\n+    monkeypatch.setenv(pakjoy.DISABLE_ENV_VAR, \"1\")\n+\n+    with pakjoy.patch_webengine():\n+        pass\n+\n+    assert not fake_open.called\n+\n+\n+class TestFindWebengineResources:\n+    @pytest.fixture\n+    def qt_data_path(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path):\n+        \"\"\"Patch qtutils.library_path() to return a temp dir.\"\"\"\n+        qt_data_path = tmp_path / \"qt_data\"\n+        qt_data_path.mkdir()\n+        monkeypatch.setattr(pakjoy.qtutils, \"library_path\", lambda _which: qt_data_path)\n+        return qt_data_path\n+\n+    @pytest.fixture\n+    def application_dir_path(\n+        self,\n+        monkeypatch: pytest.MonkeyPatch,\n+        tmp_path: pathlib.Path,\n+        qt_data_path: pathlib.Path,  # needs patching\n+    ):\n+        \"\"\"Patch QApplication.applicationDirPath() to return a temp dir.\"\"\"\n+        app_dir_path = tmp_path / \"app_dir\"\n+        app_dir_path.mkdir()\n+        monkeypatch.setattr(\n+            pakjoy.objects.qapp, \"applicationDirPath\", lambda: app_dir_path\n+        )\n+        return app_dir_path\n+\n+    @pytest.fixture\n+    def fallback_path(\n+        self,\n+        monkeypatch: pytest.MonkeyPatch,\n+        tmp_path: pathlib.Path,\n+        qt_data_path: pathlib.Path,  # needs patching\n+        application_dir_path: pathlib.Path,  # needs patching\n+    ):\n+        \"\"\"Patch the fallback path to return a temp dir.\"\"\"\n+        home_path = tmp_path / \"home\"\n+        monkeypatch.setattr(pakjoy.pathlib.Path, \"home\", lambda: home_path)\n+\n+        app_path = home_path / f\".{pakjoy.objects.qapp.applicationName()}\"\n+        app_path.mkdir(parents=True)\n+        return app_path\n+\n+    @pytest.mark.parametrize(\"create_file\", [True, False])\n+    def test_overridden(\n+        self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, create_file: bool\n+    ):\n+        \"\"\"Test the overridden path is used.\"\"\"\n+        override_path = tmp_path / \"override\"\n+        override_path.mkdir()\n+        monkeypatch.setenv(pakjoy.RESOURCES_ENV_VAR, str(override_path))\n+        if create_file:  # should get this no matter if file exists or not\n+            (override_path / pakjoy.PAK_FILENAME).touch()\n+        assert pakjoy._find_webengine_resources() == override_path\n+\n+    @pytest.mark.parametrize(\"with_subfolder\", [True, False])\n+    def test_qt_data_path(self, qt_data_path: pathlib.Path, with_subfolder: bool):\n+        \"\"\"Test qtutils.library_path() is used.\"\"\"\n+        resources_path = qt_data_path\n+        if with_subfolder:\n+            resources_path /= \"resources\"\n+            resources_path.mkdir()\n+        (resources_path / pakjoy.PAK_FILENAME).touch()\n+        assert pakjoy._find_webengine_resources() == resources_path\n+\n+    def test_application_dir_path(self, application_dir_path: pathlib.Path):\n+        \"\"\"Test QApplication.applicationDirPath() is used.\"\"\"\n+        (application_dir_path / pakjoy.PAK_FILENAME).touch()\n+        assert pakjoy._find_webengine_resources() == application_dir_path\n+\n+    def test_fallback_path(self, fallback_path: pathlib.Path):\n+        \"\"\"Test fallback path is used.\"\"\"\n+        (fallback_path / pakjoy.PAK_FILENAME).touch()\n+        assert pakjoy._find_webengine_resources() == fallback_path\n+\n+    def test_nowhere(self, fallback_path: pathlib.Path):\n+        \"\"\"Test we raise if we can't find the resources.\"\"\"\n+        with pytest.raises(\n+            FileNotFoundError, match=\"Couldn't find webengine resources dir, candidates:\\n*\"\n+        ):\n+            pakjoy._find_webengine_resources()\n+\n+\n+def json_without_comments(bytestring):\n+    str_without_comments = \"\\n\".join(\n+        [\n+            line\n+            for line in bytestring.decode(\"utf-8\").split(\"\\n\")\n+            if not line.strip().startswith(\"//\")\n+        ]\n+    )\n+    return json.loads(str_without_comments)\n+\n+\n+def read_patched_manifest():\n+    patched_resources = pathlib.Path(os.environ[pakjoy.RESOURCES_ENV_VAR])\n+\n+    with open(patched_resources / pakjoy.PAK_FILENAME, \"rb\") as fd:\n+        reparsed = pakjoy.PakParser(fd)\n+\n+    return json_without_comments(reparsed.manifest)\n+\n+\n+@pytest.mark.usefixtures(\"affected_version\")\n+class TestWithRealResourcesFile:\n+    \"\"\"Tests that use the real pak file form the Qt installation.\"\"\"\n+\n+    @skip_if_unsupported\n+    def test_happy_path(self):\n+        # Go through the full patching processes with the real resources file from\n+        # the current installation. Make sure our replacement string is in it\n+        # afterwards.\n+        with pakjoy.patch_webengine():\n+            json_manifest = read_patched_manifest()\n+\n+        assert (\n+            pakjoy.REPLACEMENT_URL.decode(\"utf-8\")\n+            in json_manifest[\"externally_connectable\"][\"matches\"]\n+        )\n+\n+    def test_copying_resources(self):\n+        # Test we managed to copy some files over\n+        work_dir = pakjoy.copy_webengine_resources()\n+\n+        assert work_dir is not None\n+        assert work_dir.exists()\n+        assert work_dir == pathlib.Path(standarddir.cache()) / pakjoy.CACHE_DIR_NAME\n+        assert (work_dir / pakjoy.PAK_FILENAME).exists()\n+        assert len(list(work_dir.glob(\"*\"))) &gt; 1\n+\n+    def test_copying_resources_overwrites(self):\n+        work_dir = pakjoy.copy_webengine_resources()\n+        assert work_dir is not None\n+        tmpfile = work_dir / \"tmp.txt\"\n+        tmpfile.touch()\n+\n+        pakjoy.copy_webengine_resources()\n+        assert not tmpfile.exists()\n+\n+    @pytest.mark.parametrize(\"osfunc\", [\"copytree\", \"rmtree\"])\n+    def test_copying_resources_oserror(self, monkeypatch, caplog, osfunc):\n+        # Test errors from the calls to shutil are handled\n+        pakjoy.copy_webengine_resources()  # run twice so we hit rmtree too\n+        caplog.clear()\n+\n+        def raiseme(err):\n+            raise err\n+\n+        monkeypatch.setattr(\n+            pakjoy.shutil, osfunc, lambda *_args: raiseme(PermissionError(osfunc))\n+        )\n+        with caplog.at_level(logging.ERROR, \"misc\"):\n+            with pakjoy.patch_webengine():\n+                pass\n+\n+        assert caplog.messages == [\n+            \"Failed to copy webengine resources, not applying quirk\"\n+        ]\n+\n+    def test_expected_file_not_found(self, cache_tmpdir, monkeypatch, caplog):\n+        with caplog.at_level(logging.ERROR, \"misc\"):\n+            pakjoy._patch(pathlib.Path(cache_tmpdir) / \"doesntexist\")\n+        assert caplog.messages[-1].startswith(\n+            \"Resource pak doesn't exist at expected location! \"\n+            \"Not applying quirks. Expected location: \"\n+        )\n+\n+\n+def json_manifest_factory(extension_id=pakjoy.HANGOUTS_MARKER, url=pakjoy.TARGET_URL):\n+    assert isinstance(extension_id, bytes)\n+    assert isinstance(url, bytes)\n+\n+    return f\"\"\"\n+    {{\n+      {extension_id.decode(\"utf-8\")}\n+      \"key\": \"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAQt2ZDdPfoSe/JI6ID5bgLHRCnCu9T36aYczmhw/tnv6QZB2I6WnOCMZXJZlRdqWc7w9jo4BWhYS50Vb4weMfh/I0On7VcRwJUgfAxW2cHB+EkmtI1v4v/OU24OqIa1Nmv9uRVeX0GjhQukdLNhAE6ACWooaf5kqKlCeK+1GOkQIDAQAB\",\n+      \"name\": \"Google Hangouts\",\n+      // Note: Always update the version number when this file is updated. Chrome\n+      // triggers extension preferences update on the version increase.\n+      \"version\": \"1.3.21\",\n+      \"manifest_version\": 2,\n+      \"externally_connectable\": {{\n+        \"matches\": [\n+          \"{url.decode(\"utf-8\")}\",\n+          \"http://localhost:*/*\"\n+        ]\n+        }}\n+    }}\n+    \"\"\".strip().encode(\n+        \"utf-8\"\n+    )\n+\n+\n+def pak_factory(version=5, entries=None, encoding=1, sentinel_position=-1):\n+    if entries is None:\n+        entries = [json_manifest_factory()]\n+\n+    buffer = io.BytesIO()\n+    buffer.write(struct.pack(\" pathlib.Path:\n+        resources_path = tmp_path / \"resources\"\n+        resources_path.mkdir()\n+\n+        buffer = pak_factory()\n+        with open(resources_path / pakjoy.PAK_FILENAME, \"wb\") as fd:\n+            fd.write(buffer.read())\n+\n+        monkeypatch.setattr(pakjoy.qtutils, \"library_path\", lambda _which: tmp_path)\n+        return resources_path\n+\n+    @pytest.fixture\n+    def quirk_dir_path(self, tmp_path: pathlib.Path) -&gt; pathlib.Path:\n+        return tmp_path / \"cache\" / pakjoy.CACHE_DIR_NAME\n+\n+    def test_patching(self, resources_path: pathlib.Path, quirk_dir_path: pathlib.Path):\n+        \"\"\"Go through the full patching processes with a fake resources file.\"\"\"\n+        with pakjoy.patch_webengine():\n+            assert os.environ[pakjoy.RESOURCES_ENV_VAR] == str(quirk_dir_path)\n+            json_manifest = read_patched_manifest()\n+\n+        assert (\n+            pakjoy.REPLACEMENT_URL.decode(\"utf-8\")\n+            in json_manifest[\"externally_connectable\"][\"matches\"]\n+        )\n+        assert pakjoy.RESOURCES_ENV_VAR not in os.environ\n+\n+    def test_preset_env_var(\n+        self,\n+        resources_path: pathlib.Path,\n+        monkeypatch: pytest.MonkeyPatch,\n+        quirk_dir_path: pathlib.Path,\n+    ):\n+        new_resources_path = resources_path.with_name(resources_path.name + \"_moved\")\n+        shutil.move(resources_path, new_resources_path)\n+        monkeypatch.setenv(pakjoy.RESOURCES_ENV_VAR, str(new_resources_path))\n+\n+        with pakjoy.patch_webengine():\n+            assert os.environ[pakjoy.RESOURCES_ENV_VAR] == str(quirk_dir_path)\n+\n+        assert os.environ[pakjoy.RESOURCES_ENV_VAR] == str(new_resources_path)\ndiff --git a/tests/unit/misc/test_pastebin.py b/tests/unit/misc/test_pastebin.py\nindex 906c4c3c6..8198b81a3 100644\n--- a/tests/unit/misc/test_pastebin.py\n+++ b/tests/unit/misc/test_pastebin.py\n@@ -1,22 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The-Compiler) \n-# Copyright 2016-2018 Anna Kobak (avk) :\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Anna Kobak (avk) :\n+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n import pytest\n from qutebrowser.qt.core import QUrl\ndiff --git a/tests/unit/misc/test_sessions.py b/tests/unit/misc/test_sessions.py\nindex 73287d15b..0591ddbbd 100644\n--- a/tests/unit/misc/test_sessions.py\n+++ b/tests/unit/misc/test_sessions.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.sessions.\"\"\"\n \n@@ -24,7 +9,7 @@ import logging\n import pytest\n import yaml\n from qutebrowser.qt.core import QUrl, QPoint, QByteArray, QObject\n-QWebView = pytest.importorskip('PyQt5.QtWebKitWidgets').QWebView\n+QWebView = pytest.importorskip('qutebrowser.qt.webkitwidgets').QWebView\n \n from qutebrowser.misc import sessions\n from qutebrowser.misc.sessions import TabHistoryItem as Item\n@@ -286,7 +271,7 @@ class FakeWebView:\n     def load_history(self, data):\n         self.loaded_history = data\n         if self.raise_error is not None:\n-            raise self.raise_error  # pylint: disable=raising-bad-type\n+            raise self.raise_error\n \n \n @pytest.fixture\ndiff --git a/tests/unit/misc/test_split.py b/tests/unit/misc/test_split.py\nindex eec923af2..f8b700982 100644\n--- a/tests/unit/misc/test_split.py\n+++ b/tests/unit/misc/test_split.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.split.\"\"\"\n \ndiff --git a/tests/unit/misc/test_split_hypothesis.py b/tests/unit/misc/test_split_hypothesis.py\nindex 9a65c56f1..356095e2a 100644\n--- a/tests/unit/misc/test_split_hypothesis.py\n+++ b/tests/unit/misc/test_split_hypothesis.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Hypothesis tests for qutebrowser.misc.split.\"\"\"\n \ndiff --git a/tests/unit/misc/test_sql.py b/tests/unit/misc/test_sql.py\nindex da72c5bb1..43f1a1d92 100644\n--- a/tests/unit/misc/test_sql.py\n+++ b/tests/unit/misc/test_sql.py\n@@ -1,24 +1,11 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Ryan Roden-Corrent (rcorre) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Ryan Roden-Corrent (rcorre) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Test the SQL API.\"\"\"\n \n+import sys\n+import sqlite3\n import pytest\n \n import hypothesis\n@@ -91,12 +78,21 @@ def test_sqlerror(klass):\n class TestSqlError:\n \n     @pytest.mark.parametrize('error_code, exception', [\n-        (sql.SqliteErrorCode.BUSY, sql.KnownError),\n-        (sql.SqliteErrorCode.CONSTRAINT, sql.BugError),\n+        (sql.SqliteErrorCode.BUSY.value, sql.KnownError),\n+        (sql.SqliteErrorCode.CONSTRAINT.value, sql.BugError),\n+        # extended error codes\n+        (\n+            sql.SqliteErrorCode.IOERR.value | (1 &lt;&lt; 8),  # SQLITE_IOERR_READ\n+            sql.KnownError\n+        ),\n+        (\n+            sql.SqliteErrorCode.CONSTRAINT.value | (1 &lt;&lt; 8),  # SQLITE_CONSTRAINT_CHECK\n+            sql.BugError\n+        ),\n     ])\n     def test_known(self, error_code, exception):\n         sql_err = QSqlError(\"driver text\", \"db text\", QSqlError.ErrorType.UnknownError,\n-                            error_code)\n+                            str(error_code))\n         with pytest.raises(exception):\n             sql.raise_sqlite_error(\"Message\", sql_err)\n \n@@ -109,7 +105,7 @@ class TestSqlError:\n                     'type: UnknownError',\n                     'database text: db text',\n                     'driver text: driver text',\n-                    'error code: 23']\n+                    'error code: 23 -&gt; SqliteErrorCode.AUTH']\n \n         assert caplog.messages == expected\n \n@@ -119,6 +115,62 @@ class TestSqlError:\n         err = klass(\"Message\", sql_err)\n         assert err.text() == \"db text\"\n \n+    @pytest.mark.parametrize(\"code\", list(sql.SqliteErrorCode))\n+    @pytest.mark.skipif(\n+        sys.version_info &lt; (3, 11),\n+        reason=\"sqlite error code constants added in Python 3.11\",\n+    )\n+    def test_sqlite_error_codes(self, code):\n+        \"\"\"Cross check our error codes with the ones in Python 3.11+.\n+\n+        See https://github.com/python/cpython/commit/86d8b465231\n+        \"\"\"\n+        pyvalue = getattr(sqlite3, f\"SQLITE_{code.name}\")\n+        assert pyvalue == code.value\n+\n+    def test_sqlite_error_codes_reverse(self):\n+        \"\"\"Check if we have all error codes defined that Python has.\n+\n+        It would be nice if this was easier (and less guesswork).\n+        However, the error codes are simply added as ints to the sqlite3 module\n+        namespace (PyModule_AddIntConstant), and lots of other constants are there too.\n+        \"\"\"\n+        # Start with all SQLITE_* names in the sqlite3 modules\n+        consts = {n for n in dir(sqlite3) if n.startswith(\"SQLITE_\")}\n+        # All error codes we know about (tested above)\n+        consts -= {f\"SQLITE_{m.name}\" for m in sql.SqliteErrorCode}\n+        # Extended error codes or other constants. From the sqlite docs:\n+        #\n+        # Primary result code symbolic names are of the form \"SQLITE_XXXXXX\"\n+        # where XXXXXX is a sequence of uppercase alphabetic characters.\n+        # Extended result code names are of the form \"SQLITE_XXXXXX_YYYYYYY\"\n+        # where the XXXXXX part is the corresponding primary result code and the\n+        # YYYYYYY is an extension that further classifies the result code.\n+        consts -= {c for c in consts if c.count(\"_\") &gt;= 2}\n+        # All remaining sqlite constants which are *not* error codes.\n+        consts -= {\n+            \"SQLITE_ANALYZE\",\n+            \"SQLITE_ATTACH\",\n+            \"SQLITE_DELETE\",\n+            \"SQLITE_DENY\",\n+            \"SQLITE_DETACH\",\n+            \"SQLITE_FUNCTION\",\n+            \"SQLITE_IGNORE\",\n+            \"SQLITE_INSERT\",\n+            \"SQLITE_PRAGMA\",\n+            \"SQLITE_READ\",\n+            \"SQLITE_RECURSIVE\",\n+            \"SQLITE_REINDEX\",\n+            \"SQLITE_SAVEPOINT\",\n+            \"SQLITE_SELECT\",\n+            \"SQLITE_TRANSACTION\",\n+            \"SQLITE_UPDATE\",\n+        }\n+        # If there is anything remaining here, either a new Python version added a new\n+        # sqlite constant which is *not* an error, or there was a new error code added.\n+        # Either add it to the set above, or to SqliteErrorCode.\n+        assert not consts\n+\n \n def test_init_table(database):\n     database.table('Foo', ['name', 'val', 'lucky'])\n@@ -406,7 +458,9 @@ class TestTransaction:\n             with database.transaction():\n                 my_table.insert({'column': 1})\n                 my_table.insert({'column': 2})\n-                raise Exception('something went horribly wrong')\n-        except Exception:\n+                raise RuntimeError(\n+                    'something went horribly wrong and the transaction will be aborted'\n+                )\n+        except RuntimeError:\n             pass\n         assert database.query('select count(*) from my_table').run().value() == 0\ndiff --git a/tests/unit/misc/test_throttle.py b/tests/unit/misc/test_throttle.py\nindex b8cc36864..c3578d3e7 100644\n--- a/tests/unit/misc/test_throttle.py\n+++ b/tests/unit/misc/test_throttle.py\n@@ -1,27 +1,12 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Jay Kamat :\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Jay Kamat :\n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.throttle.\"\"\"\n \n from unittest import mock\n \n-import sip\n+from qutebrowser.qt import sip\n import pytest\n from qutebrowser.qt.core import QObject\n \ndiff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py\nindex 2f3acc2f8..96bc8da42 100644\n--- a/tests/unit/misc/test_utilcmds.py\n+++ b/tests/unit/misc/test_utilcmds.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.misc.utilcmds.\"\"\"\n \n@@ -27,17 +12,17 @@ from qutebrowser.api import cmdutils\n from qutebrowser.utils import objreg\n \n \n-def test_repeat_command_initial(mocker, mode_manager):\n+def test_cmd_repeat_last_initial(mocker, mode_manager):\n     \"\"\"Test repeat_command first-time behavior.\n \n-    If :repeat-command is called initially, it should err, because there's\n+    If :cmd-repeat-last is called initially, it should err, because there's\n     nothing to repeat.\n     \"\"\"\n     objreg_mock = mocker.patch('qutebrowser.misc.utilcmds.objreg')\n     objreg_mock.get.return_value = mode_manager\n     with pytest.raises(cmdutils.CommandError,\n                        match=\"You didn't do anything yet.\"):\n-        utilcmds.repeat_command(win_id=0)\n+        utilcmds.cmd_repeat_last(win_id=0)\n \n \n class FakeWindow:\ndiff --git a/tests/unit/misc/userscripts/test_qute_lastpass.py b/tests/unit/misc/userscripts/test_qute_lastpass.py\nindex ebd9a7591..c05715b16 100644\n--- a/tests/unit/misc/userscripts/test_qute_lastpass.py\n+++ b/tests/unit/misc/userscripts/test_qute_lastpass.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for misc.userscripts.qute-lastpass.\"\"\"\n \n@@ -203,7 +188,7 @@ class TestQuteLastPassMain:\n         exit_code = qute_lastpass.main(arguments_mock)\n \n         assert exit_code == qute_lastpass.ExitCodes.FAILURE\n-        # pylint: disable=line-too-long\n+        # FIXME:v4 (lint): disable=line-too-long\n         stderr_mock.assert_called_with(\n             \"LastPass CLI returned for www.example.com - Error: Could not find decryption key. Perhaps you need to login with `lpass login`.\")\n         qutecommand_mock.assert_not_called()\ndiff --git a/tests/unit/scripts/test_check_coverage.py b/tests/unit/scripts/test_check_coverage.py\nindex abb0969b6..aaf69008d 100644\n--- a/tests/unit/scripts/test_check_coverage.py\n+++ b/tests/unit/scripts/test_check_coverage.py\n@@ -1,22 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n import sys\n import pathlib\ndiff --git a/tests/unit/scripts/test_dictcli.py b/tests/unit/scripts/test_dictcli.py\nindex 799c3f7ea..8752f9a59 100644\n--- a/tests/unit/scripts/test_dictcli.py\n+++ b/tests/unit/scripts/test_dictcli.py\n@@ -1,22 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2017-2021 Florian Bruhin (The-Compiler) \n-# Copyright 2017-2018 Michal Siedlaczek \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Michal Siedlaczek \n+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \n import pathlib\ndiff --git a/tests/unit/scripts/test_importer.py b/tests/unit/scripts/test_importer.py\nindex 54d6dcb12..e45a2c12d 100644\n--- a/tests/unit/scripts/test_importer.py\n+++ b/tests/unit/scripts/test_importer.py\n@@ -1,22 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2017-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n import pathlib\n import pytest\ndiff --git a/tests/unit/scripts/test_problemmatchers.py b/tests/unit/scripts/test_problemmatchers.py\nindex 79297276f..09f4a004b 100644\n--- a/tests/unit/scripts/test_problemmatchers.py\n+++ b/tests/unit/scripts/test_problemmatchers.py\n@@ -1,21 +1,7 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n import re\n \ndiff --git a/tests/unit/scripts/test_run_vulture.py b/tests/unit/scripts/test_run_vulture.py\nindex 801241310..ff79b94f6 100644\n--- a/tests/unit/scripts/test_run_vulture.py\n+++ b/tests/unit/scripts/test_run_vulture.py\n@@ -1,22 +1,9 @@\n #!/usr/bin/env python3\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n \n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n \n import sys\n import textwrap\ndiff --git a/tests/unit/test_app.py b/tests/unit/test_app.py\nindex a66f8451b..8d3c35500 100644\n--- a/tests/unit/test_app.py\n+++ b/tests/unit/test_app.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for the qutebrowser.app module.\"\"\"\n \ndiff --git a/tests/unit/test_qt_machinery.py b/tests/unit/test_qt_machinery.py\nnew file mode 100644\nindex 000000000..cf7990393\n--- /dev/null\n+++ b/tests/unit/test_qt_machinery.py\n@@ -0,0 +1,496 @@\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n+\"\"\"Test qutebrowser.qt.machinery.\"\"\"\n+\n+import re\n+import sys\n+import html\n+import argparse\n+import typing\n+from typing import Any, Optional, List, Dict, Union, Type\n+import dataclasses\n+\n+import pytest\n+\n+from qutebrowser.qt import machinery\n+\n+\n+# All global variables in machinery.py\n+MACHINERY_VARS = {\n+    \"USE_PYQT5\",\n+    \"USE_PYQT6\",\n+    \"USE_PYSIDE6\",\n+    \"IS_QT5\",\n+    \"IS_QT6\",\n+    \"IS_PYQT\",\n+    \"IS_PYSIDE\",\n+    \"INFO\",\n+}\n+# Make sure we didn't forget anything that's declared in the module.\n+# Not sure if this is a good idea. Might remove it in the future if it breaks.\n+assert set(typing.get_type_hints(machinery).keys()) == MACHINERY_VARS\n+\n+\n+@pytest.fixture(autouse=True)\n+def undo_init(monkeypatch: pytest.MonkeyPatch) -&gt; None:\n+    \"\"\"Pretend Qt support isn't initialized yet and Qt was never imported.\"\"\"\n+    monkeypatch.setattr(machinery, \"_initialized\", False)\n+    monkeypatch.delenv(\"QUTE_QT_WRAPPER\", raising=False)\n+    for wrapper in machinery.WRAPPERS:\n+        monkeypatch.delitem(sys.modules, wrapper, raising=False)\n+    for var in MACHINERY_VARS:\n+        monkeypatch.delattr(machinery, var)\n+\n+\n+@pytest.mark.parametrize(\n+    \"exception, base\",\n+    [\n+        (machinery.Unavailable(), ModuleNotFoundError),\n+        (machinery.NoWrapperAvailableError(machinery.SelectionInfo()), ImportError),\n+    ],\n+)\n+def test_importerror_exceptions(exception: Exception, base: Type[Exception]):\n+    with pytest.raises(base):\n+        raise exception\n+\n+\n+def test_selectioninfo_set_module_error():\n+    info = machinery.SelectionInfo()\n+    info.set_module_error(\"PyQt5\", ImportError(\"Python imploded\"))\n+    assert info == machinery.SelectionInfo(\n+        wrapper=None,\n+        reason=machinery.SelectionReason.unknown,\n+        outcomes={\"PyQt5\": \"ImportError: Python imploded\"},\n+    )\n+\n+\n+def test_selectioninfo_use_wrapper():\n+    info = machinery.SelectionInfo()\n+    info.use_wrapper(\"PyQt6\")\n+    assert info == machinery.SelectionInfo(\n+        wrapper=\"PyQt6\",\n+        reason=machinery.SelectionReason.unknown,\n+        outcomes={\"PyQt6\": \"success\"},\n+    )\n+\n+\n+@pytest.mark.parametrize(\n+    \"info, expected\",\n+    [\n+        (\n+            machinery.SelectionInfo(\n+                wrapper=\"PyQt5\",\n+                reason=machinery.SelectionReason.cli,\n+            ),\n+            \"Qt wrapper: PyQt5 (via --qt-wrapper)\",\n+        ),\n+        (\n+            machinery.SelectionInfo(\n+                wrapper=\"PyQt6\",\n+                reason=machinery.SelectionReason.env,\n+            ),\n+            \"Qt wrapper: PyQt6 (via QUTE_QT_WRAPPER)\",\n+        ),\n+        (\n+            machinery.SelectionInfo(\n+                wrapper=\"PyQt6\",\n+                reason=machinery.SelectionReason.auto,\n+                outcomes={\n+                    \"PyQt6\": \"success\",\n+                    \"PyQt5\": \"ImportError: Python imploded\",\n+                },\n+            ),\n+            (\n+                \"Qt wrapper info:\\n\"\n+                \"  PyQt6: success\\n\"\n+                \"  PyQt5: ImportError: Python imploded\\n\"\n+                \"  -&gt; selected: PyQt6 (via autoselect)\"\n+            ),\n+        ),\n+    ],\n+)\n+def test_selectioninfo_str(info: machinery.SelectionInfo, expected: str):\n+    assert str(info) == expected\n+    # The test is somewhat duplicating the logic here, but it's a good sanity check.\n+    assert info.to_html() == html.escape(expected).replace(\"\\n\", \"\")\n+\n+\n+@pytest.mark.parametrize(\"order\", [[\"PyQt5\", \"PyQt6\"], [\"PyQt6\", \"PyQt5\"]])\n+def test_selectioninfo_str_wrapper_precedence(order: List[str]):\n+    \"\"\"The order of the wrappers should be the same as in machinery.WRAPPERS.\"\"\"\n+    info = machinery.SelectionInfo(\n+        wrapper=\"PyQt6\",\n+        reason=machinery.SelectionReason.auto,\n+    )\n+    for module in order:\n+        info.set_module_error(module, ImportError(\"Python imploded\"))\n+\n+    lines = str(info).splitlines()[1:-1]\n+    wrappers = [line.split(\":\")[0].strip() for line in lines]\n+    assert wrappers == machinery.WRAPPERS\n+\n+\n+@pytest.fixture\n+def modules():\n+    \"\"\"Return a dict of modules to import-patch, all unavailable by default.\"\"\"\n+    return dict.fromkeys(machinery.WRAPPERS, False)\n+\n+\n+@pytest.mark.parametrize(\n+    \"available, expected\",\n+    [\n+        pytest.param(\n+            {\n+                \"PyQt5\": ModuleNotFoundError(\"hiding somewhere\"),\n+                \"PyQt6\": ModuleNotFoundError(\"hiding somewhere\"),\n+            },\n+            machinery.SelectionInfo(\n+                wrapper=None,\n+                reason=machinery.SelectionReason.auto,\n+                outcomes={\n+                    \"PyQt5\": \"ModuleNotFoundError: hiding somewhere\",\n+                    \"PyQt6\": \"ModuleNotFoundError: hiding somewhere\",\n+                },\n+            ),\n+            id=\"none-available\",\n+        ),\n+        pytest.param(\n+            {\n+                \"PyQt5\": ModuleNotFoundError(\"hiding somewhere\"),\n+                \"PyQt6\": True,\n+            },\n+            machinery.SelectionInfo(\n+                wrapper=\"PyQt6\",\n+                reason=machinery.SelectionReason.auto,\n+                outcomes={\"PyQt6\": \"success\"},\n+            ),\n+            id=\"only-pyqt6\",\n+        ),\n+        pytest.param(\n+            {\n+                \"PyQt5\": True,\n+                \"PyQt6\": ModuleNotFoundError(\"hiding somewhere\"),\n+            },\n+            machinery.SelectionInfo(\n+                wrapper=\"PyQt5\",\n+                reason=machinery.SelectionReason.auto,\n+                outcomes={\n+                    \"PyQt6\": \"ModuleNotFoundError: hiding somewhere\",\n+                    \"PyQt5\": \"success\",\n+                },\n+            ),\n+            id=\"only-pyqt5\",\n+        ),\n+        pytest.param(\n+            {\"PyQt5\": True, \"PyQt6\": True},\n+            machinery.SelectionInfo(\n+                wrapper=\"PyQt6\",\n+                reason=machinery.SelectionReason.auto,\n+                outcomes={\"PyQt6\": \"success\"},\n+            ),\n+            id=\"both\",\n+        ),\n+        pytest.param(\n+            {\n+                \"PyQt6\": ImportError(\"Fake ImportError for PyQt6.\"),\n+                \"PyQt5\": True,\n+            },\n+            machinery.SelectionInfo(\n+                wrapper=None,\n+                reason=machinery.SelectionReason.auto,\n+                outcomes={\n+                    \"PyQt6\": \"ImportError: Fake ImportError for PyQt6.\",\n+                },\n+            ),\n+            id=\"import-error\",\n+        ),\n+    ],\n+)\n+def test_autoselect(\n+    stubs: Any,\n+    available: Dict[str, Union[bool, Exception]],\n+    expected: machinery.SelectionInfo,\n+    monkeypatch: pytest.MonkeyPatch,\n+):\n+    stubs.ImportFake(available, monkeypatch).patch()\n+    assert machinery._autoselect_wrapper() == expected\n+\n+\n+@dataclasses.dataclass\n+class SelectWrapperCase:\n+    name: str\n+    expected: machinery.SelectionInfo\n+    args: Optional[argparse.Namespace] = None\n+    env: Optional[str] = None\n+    override: Optional[str] = None\n+\n+    def __str__(self):\n+        return self.name\n+\n+\n+class TestSelectWrapper:\n+    @pytest.mark.parametrize(\n+        \"tc\",\n+        [\n+            # Only argument given\n+            SelectWrapperCase(\n+                \"pyqt6-arg\",\n+                args=argparse.Namespace(qt_wrapper=\"PyQt6\"),\n+                expected=machinery.SelectionInfo(\n+                    wrapper=\"PyQt6\", reason=machinery.SelectionReason.cli\n+                ),\n+            ),\n+            SelectWrapperCase(\n+                \"pyqt5-arg\",\n+                args=argparse.Namespace(qt_wrapper=\"PyQt5\"),\n+                expected=machinery.SelectionInfo(\n+                    wrapper=\"PyQt5\", reason=machinery.SelectionReason.cli\n+                ),\n+            ),\n+            SelectWrapperCase(\n+                \"pyqt6-arg-empty-env\",\n+                args=argparse.Namespace(qt_wrapper=\"PyQt5\"),\n+                env=\"\",\n+                expected=machinery.SelectionInfo(\n+                    wrapper=\"PyQt5\", reason=machinery.SelectionReason.cli\n+                ),\n+            ),\n+            # Only environment variable given\n+            SelectWrapperCase(\n+                \"pyqt6-env\",\n+                env=\"PyQt6\",\n+                expected=machinery.SelectionInfo(\n+                    wrapper=\"PyQt6\", reason=machinery.SelectionReason.env\n+                ),\n+            ),\n+            SelectWrapperCase(\n+                \"pyqt5-env\",\n+                env=\"PyQt5\",\n+                expected=machinery.SelectionInfo(\n+                    wrapper=\"PyQt5\", reason=machinery.SelectionReason.env\n+                ),\n+            ),\n+            # Both given\n+            SelectWrapperCase(\n+                \"pyqt5-arg-pyqt6-env\",\n+                args=argparse.Namespace(qt_wrapper=\"PyQt5\"),\n+                env=\"PyQt6\",\n+                expected=machinery.SelectionInfo(\n+                    wrapper=\"PyQt5\", reason=machinery.SelectionReason.cli\n+                ),\n+            ),\n+            SelectWrapperCase(\n+                \"pyqt6-arg-pyqt5-env\",\n+                args=argparse.Namespace(qt_wrapper=\"PyQt6\"),\n+                env=\"PyQt5\",\n+                expected=machinery.SelectionInfo(\n+                    wrapper=\"PyQt6\", reason=machinery.SelectionReason.cli\n+                ),\n+            ),\n+            SelectWrapperCase(\n+                \"pyqt6-arg-pyqt6-env\",\n+                args=argparse.Namespace(qt_wrapper=\"PyQt6\"),\n+                env=\"PyQt6\",\n+                expected=machinery.SelectionInfo(\n+                    wrapper=\"PyQt6\", reason=machinery.SelectionReason.cli\n+                ),\n+            ),\n+            # Override\n+            SelectWrapperCase(\n+                \"override-only\",\n+                override=\"PyQt6\",\n+                expected=machinery.SelectionInfo(\n+                    wrapper=\"PyQt6\", reason=machinery.SelectionReason.override\n+                ),\n+            ),\n+            SelectWrapperCase(\n+                \"override-arg\",\n+                args=argparse.Namespace(qt_wrapper=\"PyQt5\"),\n+                override=\"PyQt6\",\n+                expected=machinery.SelectionInfo(\n+                    wrapper=\"PyQt5\", reason=machinery.SelectionReason.cli\n+                ),\n+            ),\n+            SelectWrapperCase(\n+                \"override-env\",\n+                env=\"PyQt5\",\n+                override=\"PyQt6\",\n+                expected=machinery.SelectionInfo(\n+                    wrapper=\"PyQt5\", reason=machinery.SelectionReason.env\n+                ),\n+            ),\n+        ],\n+        ids=str,\n+    )\n+    def test_select(self, tc: SelectWrapperCase, monkeypatch: pytest.MonkeyPatch):\n+        if tc.env is None:\n+            monkeypatch.delenv(\"QUTE_QT_WRAPPER\", raising=False)\n+        else:\n+            monkeypatch.setenv(\"QUTE_QT_WRAPPER\", tc.env)\n+\n+        if tc.override is not None:\n+            monkeypatch.setattr(machinery, \"_WRAPPER_OVERRIDE\", tc.override)\n+\n+        assert machinery._select_wrapper(tc.args) == tc.expected\n+\n+    @pytest.mark.parametrize(\n+        \"args, env\",\n+        [\n+            (None, None),\n+            (argparse.Namespace(qt_wrapper=None), None),\n+            (argparse.Namespace(qt_wrapper=None), \"\"),\n+        ],\n+    )\n+    def test_autoselect_by_default(\n+        self,\n+        args: Optional[argparse.Namespace],\n+        env: Optional[str],\n+        monkeypatch: pytest.MonkeyPatch,\n+    ):\n+        \"\"\"Test that the default behavior is to autoselect a wrapper.\n+\n+        Autoselection itself is tested further down.\n+        \"\"\"\n+        if env is None:\n+            monkeypatch.delenv(\"QUTE_QT_WRAPPER\", raising=False)\n+        else:\n+            monkeypatch.setenv(\"QUTE_QT_WRAPPER\", env)\n+\n+        assert machinery._select_wrapper(args).reason == machinery.SelectionReason.auto\n+\n+    def test_after_qt_import(self, monkeypatch: pytest.MonkeyPatch):\n+        monkeypatch.setitem(sys.modules, \"PyQt6\", None)\n+        with pytest.warns(UserWarning, match=\"PyQt6 already imported\"):\n+            machinery._select_wrapper(args=None)\n+\n+    def test_invalid_override(self, monkeypatch: pytest.MonkeyPatch):\n+        monkeypatch.setattr(machinery, \"_WRAPPER_OVERRIDE\", \"invalid\")\n+        with pytest.raises(AssertionError):\n+            machinery._select_wrapper(args=None)\n+\n+\n+class TestInit:\n+    @pytest.fixture\n+    def empty_args(self) -&gt; argparse.Namespace:\n+        return argparse.Namespace(qt_wrapper=None)\n+\n+    def test_multiple_implicit(self, monkeypatch: pytest.MonkeyPatch):\n+        monkeypatch.setattr(machinery, \"_initialized\", True)\n+        machinery.init_implicit()\n+        machinery.init_implicit()\n+\n+    def test_multiple_explicit(\n+        self,\n+        monkeypatch: pytest.MonkeyPatch,\n+        empty_args: argparse.Namespace,\n+    ):\n+        monkeypatch.setattr(machinery, \"_initialized\", True)\n+\n+        with pytest.raises(\n+            machinery.Error, match=r\"init\\(\\) already called before application init\"\n+        ):\n+            machinery.init(args=empty_args)\n+\n+    @pytest.fixture(params=[\"auto\", \"\", None])\n+    def qt_auto_env(\n+        self,\n+        request: pytest.FixtureRequest,\n+        monkeypatch: pytest.MonkeyPatch,\n+    ):\n+        \"\"\"Trigger wrapper autoselection via environment variable.\n+\n+        Autoselection should be used in three scenarios:\n+\n+        - The environment variable is set to \"auto\".\n+        - The environment variable is set to an empty string.\n+        - The environment variable is not set at all.\n+\n+        We run test_none_available_*() for all three scenarios.\n+        \"\"\"\n+        if request.param is None:\n+            monkeypatch.delenv(\"QUTE_QT_WRAPPER\", raising=False)\n+        else:\n+            monkeypatch.setenv(\"QUTE_QT_WRAPPER\", request.param)\n+\n+    def test_none_available_implicit(\n+        self,\n+        stubs: Any,\n+        modules: Dict[str, bool],\n+        monkeypatch: pytest.MonkeyPatch,\n+        qt_auto_env: None,\n+    ):\n+        stubs.ImportFake(modules, monkeypatch).patch()\n+\n+        message_lines = [\n+            \"No Qt wrapper was importable.\",\n+            \"\",\n+            \"Qt wrapper info:\",\n+            \"  PyQt6: ImportError: Fake ImportError for PyQt6.\",\n+            \"  PyQt5: not imported\",\n+            \"  -&gt; selected: None (via autoselect)\",\n+        ]\n+\n+        with pytest.raises(\n+            machinery.NoWrapperAvailableError,\n+            match=re.escape(\"\\n\".join(message_lines)),\n+        ):\n+            machinery.init_implicit()\n+\n+    def test_none_available_explicit(\n+        self,\n+        stubs: Any,\n+        modules: Dict[str, bool],\n+        monkeypatch: pytest.MonkeyPatch,\n+        empty_args: argparse.Namespace,\n+        qt_auto_env: None,\n+    ):\n+        stubs.ImportFake(modules, monkeypatch).patch()\n+\n+        info = machinery.init(args=empty_args)\n+        assert info == machinery.SelectionInfo(\n+            wrapper=None,\n+            reason=machinery.SelectionReason.auto,\n+            outcomes={\n+                \"PyQt6\": \"ImportError: Fake ImportError for PyQt6.\",\n+            },\n+        )\n+\n+    @pytest.mark.parametrize(\n+        \"selected_wrapper, true_vars\",\n+        [\n+            (\"PyQt6\", [\"USE_PYQT6\", \"IS_QT6\", \"IS_PYQT\"]),\n+            (\"PyQt5\", [\"USE_PYQT5\", \"IS_QT5\", \"IS_PYQT\"]),\n+            (\"PySide6\", [\"USE_PYSIDE6\", \"IS_QT6\", \"IS_PYSIDE\"]),\n+        ],\n+    )\n+    @pytest.mark.parametrize(\"explicit\", [True, False])\n+    def test_properly(\n+        self,\n+        monkeypatch: pytest.MonkeyPatch,\n+        selected_wrapper: str,\n+        true_vars: str,\n+        explicit: bool,\n+        empty_args: argparse.Namespace,\n+    ):\n+        info = machinery.SelectionInfo(\n+            wrapper=selected_wrapper,\n+            reason=machinery.SelectionReason.fake,\n+        )\n+        monkeypatch.setattr(machinery, \"_select_wrapper\", lambda args: info)\n+\n+        if explicit:\n+            ret = machinery.init(empty_args)\n+            assert ret == info\n+        else:\n+            machinery.init_implicit()\n+\n+        assert machinery.INFO == info\n+\n+        bool_vars = MACHINERY_VARS - {\"INFO\"}\n+        expected_vars = dict.fromkeys(bool_vars, False)\n+        expected_vars.update(dict.fromkeys(true_vars, True))\n+        actual_vars = {var: getattr(machinery, var) for var in bool_vars}\n+\n+        assert expected_vars == actual_vars\ndiff --git a/tests/unit/test_qutebrowser.py b/tests/unit/test_qutebrowser.py\nindex 36b4065a1..65ca6379d 100644\n--- a/tests/unit/test_qutebrowser.py\n+++ b/tests/unit/test_qutebrowser.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2020-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.qutebrowser.\n \ndiff --git a/tests/unit/utils/overflow_test_cases.py b/tests/unit/utils/overflow_test_cases.py\nindex 2f3368be3..d88435637 100644\n--- a/tests/unit/utils/overflow_test_cases.py\n+++ b/tests/unit/utils/overflow_test_cases.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Provides test data for overflow checking.\n \ndiff --git a/tests/unit/utils/test_debug.py b/tests/unit/utils/test_debug.py\nindex ea4930aa3..e9d9c2861 100644\n--- a/tests/unit/utils/test_debug.py\n+++ b/tests/unit/utils/test_debug.py\n@@ -1,21 +1,6 @@\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.debug.\"\"\"\n \n@@ -28,7 +13,7 @@ import pytest\n from qutebrowser.qt.core import pyqtSignal, Qt, QEvent, QObject, QTimer\n from qutebrowser.qt.widgets import QStyle, QFrame, QSpinBox\n \n-from qutebrowser.utils import debug\n+from qutebrowser.utils import debug, qtutils\n from qutebrowser.misc import objects\n \n \n@@ -138,15 +123,17 @@ class TestQEnumKey:\n         (QFrame, 0x0030, QFrame.Shadow, 'Sunken'),\n         (QFrame, 0x1337, QFrame.Shadow, '0x1337'),\n         (Qt, Qt.AnchorPoint.AnchorLeft, None, 'AnchorLeft'),\n+\n+        # No static meta object, passing in an int on Qt 6\n+        (QEvent, qtutils.extract_enum_val(QEvent.Type.User), QEvent.Type, 'User'),\n+\n+        # Unknown value with IntFlags\n+        (Qt, Qt.AlignmentFlag(1024), None, '0x0400'),\n     ])\n     def test_qenum_key(self, base, value, klass, expected):\n         key = debug.qenum_key(base, value, klass=klass)\n         assert key == expected\n \n-    def test_add_base(self):\n-        key = debug.qenum_key(QFrame, QFrame.Shadow.Sunken, add_base=True)\n-        assert key == 'QFrame.Shadow.Sunken'\n-\n     def test_int_noklass(self):\n         \"\"\"Test passing an int without explicit klass given.\"\"\"\n         with pytest.raises(TypeError):\n@@ -160,18 +147,20 @@ class TestQFlagsKey:\n     https://github.com/qutebrowser/qutebrowser/issues/42\n     \"\"\"\n \n-    fixme = pytest.mark.xfail(reason=\"See issue #42\", raises=AssertionError)\n-\n     @pytest.mark.parametrize('base, value, klass, expected', [\n         (Qt, Qt.AlignmentFlag.AlignTop, None, 'AlignTop'),\n         pytest.param(Qt, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop, None,\n-                     'AlignLeft|AlignTop', marks=fixme),\n+                     'AlignLeft|AlignTop', marks=pytest.mark.qt5_xfail(raises=AssertionError)),\n         (Qt, Qt.AlignmentFlag.AlignCenter, None, 'AlignHCenter|AlignVCenter'),\n-        pytest.param(Qt, 0x0021, Qt.Alignment, 'AlignLeft|AlignTop',\n-                     marks=fixme),\n-        (Qt, 0x1100, Qt.Alignment, '0x0100|0x1000'),\n-        (Qt, Qt.DockWidgetAreas(0), Qt.DockWidgetArea, 'NoDockWidgetArea'),\n-        (Qt, Qt.DockWidgetAreas(0), None, '0x0000'),\n+        pytest.param(Qt, 0x0021, Qt.AlignmentFlag, 'AlignLeft|AlignTop',\n+                     marks=pytest.mark.qt5_xfail(raises=AssertionError)),\n+        (Qt, 0x1100, Qt.AlignmentFlag, 'AlignBaseline|0x1000'),\n+        (Qt, Qt.DockWidgetArea(0), Qt.DockWidgetArea, 'NoDockWidgetArea'),\n+        (Qt, Qt.DockWidgetArea(0), None, 'NoDockWidgetArea'),\n+        (Qt, Qt.KeyboardModifier.ShiftModifier, Qt.KeyboardModifier, 'ShiftModifier'),\n+        (Qt, Qt.KeyboardModifier.ShiftModifier, None, 'ShiftModifier'),\n+        (Qt, Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier, Qt.KeyboardModifier, 'ShiftModifier|ControlModifier'),\n+        pytest.param(Qt, Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier, None, 'ShiftModifier|ControlModifier', marks=pytest.mark.qt5_xfail(raises=AssertionError)),\n     ])\n     def test_qflags_key(self, base, value, klass, expected):\n         flags = debug.qflags_key(base, value, klass=klass)\n@@ -186,7 +175,7 @@ class TestQFlagsKey:\n \n         No idea what's happening here exactly...\n         \"\"\"\n-        qwebpage = pytest.importorskip(\"PyQt5.QtWebKitWidgets\").QWebPage\n+        qwebpage = pytest.importorskip(\"qutebrowser.qt.webkitwidgets\").QWebPage\n \n         flags = qwebpage.FindWrapsAroundDocument\n         flags |= qwebpage.FindBackward\n@@ -197,11 +186,6 @@ class TestQFlagsKey:\n                          flags,\n                          klass=qwebpage.FindFlag)\n \n-    def test_add_base(self):\n-        \"\"\"Test with add_base=True.\"\"\"\n-        flags = debug.qflags_key(Qt, Qt.AlignmentFlag.AlignTop, add_base=True)\n-        assert flags == 'Qt.AlignmentFlag.AlignTop'\n-\n     def test_int_noklass(self):\n         \"\"\"Test passing an int without explicit klass given.\"\"\"\n         with pytest.raises(TypeError):\n@@ -297,6 +281,6 @@ class TestGetAllObjects:\n     def test_get_all_objects_qapp(self, qapp, monkeypatch):\n         monkeypatch.setattr(objects, 'qapp', qapp)\n         objs = debug.get_all_objects()\n-        event_dispatcher = '\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.error.\"\"\"\n \ndiff --git a/tests/unit/utils/test_javascript.py b/tests/unit/utils/test_javascript.py\nindex 7a97ef6d1..d09a27054 100644\n--- a/tests/unit/utils/test_javascript.py\n+++ b/tests/unit/utils/test_javascript.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.javascript.\"\"\"\n \ndiff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py\nindex 28ff09525..458109456 100644\n--- a/tests/unit/utils/test_jinja.py\n+++ b/tests/unit/utils/test_jinja.py\n@@ -1,21 +1,6 @@\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.jinja.\"\"\"\n \ndiff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py\nindex 6865e6e0d..03ace4009 100644\n--- a/tests/unit/utils/test_log.py\n+++ b/tests/unit/utils/test_log.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.log.\"\"\"\n \n@@ -24,11 +9,9 @@ import argparse\n import itertools\n import sys\n import warnings\n-import dataclasses\n \n import pytest\n import _pytest.logging  # pylint: disable=import-private-name\n-from qutebrowser.qt import core as QtCore\n \n from qutebrowser import qutebrowser\n from qutebrowser.utils import log\n@@ -243,7 +226,7 @@ class TestInitLog:\n \n     @pytest.fixture(autouse=True)\n     def setup(self, mocker):\n-        mocker.patch('qutebrowser.utils.log.QtCore.qInstallMessageHandler',\n+        mocker.patch('qutebrowser.utils.qtlog.qtcore.qInstallMessageHandler',\n                      autospec=True)\n         yield\n         # Make sure logging is in a sensible default state\n@@ -344,35 +327,6 @@ class TestInitLog:\n         assert log.console_filter.names == {'misc'}\n \n \n-class TestHideQtWarning:\n-\n-    \"\"\"Tests for hide_qt_warning/QtWarningFilter.\"\"\"\n-\n-    @pytest.fixture\n-    def qt_logger(self):\n-        return logging.getLogger('qt-tests')\n-\n-    def test_unfiltered(self, qt_logger, caplog):\n-        with log.hide_qt_warning(\"World\", 'qt-tests'):\n-            with caplog.at_level(logging.WARNING, 'qt-tests'):\n-                qt_logger.warning(\"Hello World\")\n-        assert len(caplog.records) == 1\n-        record = caplog.records[0]\n-        assert record.levelname == 'WARNING'\n-        assert record.message == \"Hello World\"\n-\n-    @pytest.mark.parametrize('line', [\n-        \"Hello\",  # exact match\n-        \"Hello World\",  # match at start of line\n-        \"  Hello World  \",  # match with spaces\n-    ])\n-    def test_filtered(self, qt_logger, caplog, line):\n-        with log.hide_qt_warning(\"Hello\", 'qt-tests'):\n-            with caplog.at_level(logging.WARNING, 'qt-tests'):\n-                qt_logger.warning(line)\n-        assert not caplog.records\n-\n-\n @pytest.mark.parametrize('suffix, expected', [\n     ('', 'STUB: test_stub'),\n     ('foo', 'STUB: test_stub (foo)'),\n@@ -407,27 +361,3 @@ def test_warning_still_errors():\n     # Mainly a sanity check after the tests messing with warnings above.\n     with pytest.raises(UserWarning):\n         warnings.warn(\"error\", UserWarning)\n-\n-\n-class TestQtMessageHandler:\n-\n-    @dataclasses.dataclass\n-    class Context:\n-\n-        \"\"\"Fake QMessageLogContext.\"\"\"\n-\n-        function: str = None\n-        category: str = None\n-        file: str = None\n-        line: int = None\n-\n-    @pytest.fixture(autouse=True)\n-    def init_args(self):\n-        parser = qutebrowser.get_argparser()\n-        args = parser.parse_args([])\n-        log.init_log(args)\n-\n-    def test_empty_message(self, caplog):\n-        \"\"\"Make sure there's no crash with an empty message.\"\"\"\n-        log.qt_message_handler(QtCore.QtMsgType.QtDebugMsg, self.Context(), \"\")\n-        assert caplog.messages == [\"Logged empty message!\"]\ndiff --git a/tests/unit/utils/test_qtlog.py b/tests/unit/utils/test_qtlog.py\nnew file mode 100644\nindex 000000000..8db9fdc65\n--- /dev/null\n+++ b/tests/unit/utils/test_qtlog.py\n@@ -0,0 +1,68 @@\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n+#\n+# SPDX-License-Identifier: GPL-3.0-or-later\n+\n+\"\"\"Tests for qutebrowser.utils.qtlog.\"\"\"\n+\n+import dataclasses\n+import logging\n+\n+import pytest\n+\n+from qutebrowser import qutebrowser\n+from qutebrowser.utils import qtlog\n+\n+from qutebrowser.qt import core as qtcore\n+\n+\n+class TestQtMessageHandler:\n+\n+    @dataclasses.dataclass\n+    class Context:\n+\n+        \"\"\"Fake QMessageLogContext.\"\"\"\n+\n+        function: str = None\n+        category: str = None\n+        file: str = None\n+        line: int = None\n+\n+    @pytest.fixture(autouse=True)\n+    def init_args(self):\n+        parser = qutebrowser.get_argparser()\n+        args = parser.parse_args([])\n+        qtlog.init(args)\n+\n+    def test_empty_message(self, caplog):\n+        \"\"\"Make sure there's no crash with an empty message.\"\"\"\n+        qtlog.qt_message_handler(qtcore.QtMsgType.QtDebugMsg, self.Context(), \"\")\n+        assert caplog.messages == [\"Logged empty message!\"]\n+\n+\n+class TestHideQtWarning:\n+\n+    \"\"\"Tests for hide_qt_warning/QtWarningFilter.\"\"\"\n+\n+    @pytest.fixture\n+    def qt_logger(self):\n+        return logging.getLogger('qt-tests')\n+\n+    def test_unfiltered(self, qt_logger, caplog):\n+        with qtlog.hide_qt_warning(\"World\", 'qt-tests'):\n+            with caplog.at_level(logging.WARNING, 'qt-tests'):\n+                qt_logger.warning(\"Hello World\")\n+        assert len(caplog.records) == 1\n+        record = caplog.records[0]\n+        assert record.levelname == 'WARNING'\n+        assert record.message == \"Hello World\"\n+\n+    @pytest.mark.parametrize('line', [\n+        \"Hello\",  # exact match\n+        \"Hello World\",  # match at start of line\n+        \"  Hello World  \",  # match with spaces\n+    ])\n+    def test_filtered(self, qt_logger, caplog, line):\n+        with qtlog.hide_qt_warning(\"Hello\", 'qt-tests'):\n+            with caplog.at_level(logging.WARNING, 'qt-tests'):\n+                qt_logger.warning(line)\n+        assert not caplog.records\ndiff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py\nindex 10042bb14..0a3afa416 100644\n--- a/tests/unit/utils/test_qtutils.py\n+++ b/tests/unit/utils/test_qtutils.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.qtutils.\"\"\"\n \n@@ -29,8 +13,9 @@ import unittest.mock\n \n import pytest\n from qutebrowser.qt.core import (QDataStream, QPoint, QUrl, QByteArray, QIODevice,\n-                          QTimer, QBuffer, QFile, QProcess, QFileDevice, QLibraryInfo)\n+                          QTimer, QBuffer, QFile, QProcess, QFileDevice, QLibraryInfo, Qt, QObject)\n from qutebrowser.qt.gui import QColor\n+from qutebrowser.qt import sip\n \n from qutebrowser.utils import qtutils, utils, usertypes\n import overflow_test_cases\n@@ -125,6 +110,17 @@ def test_is_single_process(monkeypatch, stubs, backend, arguments, single_proces\n     assert qtutils.is_single_process() == single_process\n \n \n+@pytest.mark.parametrize('platform, is_wayland', [\n+    (\"wayland\", True),\n+    (\"wayland-egl\", True),\n+    (\"xcb\", False),\n+])\n+def test_is_wayland(monkeypatch, stubs, platform, is_wayland):\n+    qapp = stubs.FakeQApplication(platform_name=platform)\n+    monkeypatch.setattr(qtutils.objects, 'qapp', qapp)\n+    assert qtutils.is_wayland() == is_wayland\n+\n+\n class TestCheckOverflow:\n \n     \"\"\"Test check_overflow.\"\"\"\n@@ -206,12 +202,24 @@ def test_ensure_valid(obj, raising, exc_reason, exc_str):\n \n @pytest.mark.parametrize('status, raising, message', [\n     (QDataStream.Status.Ok, False, None),\n-    (QDataStream.Status.ReadPastEnd, True, \"The data stream has read past the end of \"\n-                                    \"the data in the underlying device.\"),\n-    (QDataStream.Status.ReadCorruptData, True, \"The data stream has read corrupt \"\n-                                        \"data.\"),\n-    (QDataStream.Status.WriteFailed, True, \"The data stream cannot write to the \"\n-                                    \"underlying device.\"),\n+    (QDataStream.Status.ReadPastEnd, True,\n+     \"The data stream has read past the end of the data in the underlying device.\"),\n+    (QDataStream.Status.ReadCorruptData, True,\n+     \"The data stream has read corrupt data.\"),\n+    (QDataStream.Status.WriteFailed, True,\n+     \"The data stream cannot write to the underlying device.\"),\n+    pytest.param(\n+        getattr(QDataStream.Status, \"SizeLimitExceeded\", None),\n+        True,\n+        (\n+            \"The data stream cannot read or write the data because its size is larger \"\n+            \"than supported by the current platform.\"\n+        ),\n+        marks=pytest.mark.skipif(\n+            not hasattr(QDataStream.Status, \"SizeLimitExceeded\"),\n+            reason=\"Added in Qt 6.7\"\n+        )\n+    ),\n ])\n def test_check_qdatastream(status, raising, message):\n     \"\"\"Test check_qdatastream.\n@@ -230,11 +238,25 @@ def test_check_qdatastream(status, raising, message):\n         qtutils.check_qdatastream(stream)\n \n \n-def test_qdatastream_status_count():\n-    \"\"\"Make sure no new members are added to QDataStream.Status.\"\"\"\n-    values = vars(QDataStream).values()\n-    status_vals = [e for e in values if isinstance(e, QDataStream.Status)]\n-    assert len(status_vals) == 4\n+def test_qdatastream_status_members():\n+    \"\"\"Make sure no new members are added to QDataStream.Status.\n+\n+    If this fails, qtutils.check_qdatastream will need to be updated with the\n+    respective error documentation.\n+    \"\"\"\n+    status_vals = set(testutils.enum_members(QDataStream, QDataStream.Status).values())\n+    expected = {\n+        QDataStream.Status.Ok,\n+        QDataStream.Status.ReadPastEnd,\n+        QDataStream.Status.ReadCorruptData,\n+        QDataStream.Status.WriteFailed,\n+    }\n+    try:\n+        expected.add(QDataStream.Status.SizeLimitExceeded)\n+    except AttributeError:\n+        # Added in Qt 6.7\n+        pass\n+    assert status_vals == expected\n \n \n @pytest.mark.parametrize('color, expected', [\n@@ -301,7 +323,7 @@ class TestSerializeStream:\n                                           \"data.\"):\n             qtutils.serialize_stream(stream_mock, obj)\n \n-        assert stream_mock.__lshift__.called_once_with(obj)\n+        stream_mock.__lshift__.assert_called_once_with(obj)\n \n     def test_deserialize_pre_error_mock(self, stream_mock):\n         \"\"\"Test deserialize_stream with an error already set.\"\"\"\n@@ -323,7 +345,7 @@ class TestSerializeStream:\n                                           \"data.\"):\n             qtutils.deserialize_stream(stream_mock, obj)\n \n-        assert stream_mock.__rshift__.called_once_with(obj)\n+        stream_mock.__rshift__.assert_called_once_with(obj)\n \n     def test_round_trip_real_stream(self):\n         \"\"\"Test a round trip with a real QDataStream.\"\"\"\n@@ -736,8 +758,10 @@ class TestPyQIODevice:\n         # pylint: enable=no-member,useless-suppression\n         else:\n             pytest.skip(\"Needs os.SEEK_HOLE or os.SEEK_DATA available.\")\n+\n         pyqiodev.open(QIODevice.OpenModeFlag.ReadOnly)\n         with pytest.raises(io.UnsupportedOperation):\n+            # pylint: disable=possibly-used-before-assignment\n             pyqiodev.seek(0, whence)\n \n     @pytest.mark.flaky\n@@ -1037,15 +1061,81 @@ class TestLibraryPath:\n     def test_simple(self):\n         try:\n             # Qt 6\n-            path = QLibraryInfo.path(QLibraryInfo.LibraryPath.DataLocation)\n+            path = QLibraryInfo.path(QLibraryInfo.LibraryPath.DataPath)\n         except AttributeError:\n             # Qt 5\n-            path = QLibraryInfo.location(QLibraryInfo.LibraryLocation.DataLocation)\n+            path = QLibraryInfo.location(QLibraryInfo.LibraryLocation.DataPath)\n \n         assert path\n-        assert str(qtutils.library_path(qtutils.LibraryPath.data)) == path\n+        assert qtutils.library_path(qtutils.LibraryPath.data).as_posix() == path\n \n     @pytest.mark.parametrize(\"which\", list(qtutils.LibraryPath))\n     def test_all(self, which):\n-        path = qtutils.library_path(which)\n-        assert path.exists()\n+        if utils.is_windows and which == qtutils.LibraryPath.settings:\n+            pytest.skip(\"Settings path not supported on Windows\")\n+        qtutils.library_path(which)\n+        # The returned path doesn't necessarily exist.\n+\n+    def test_values_match_qt(self):\n+        try:\n+            # Qt 6\n+            enumtype = QLibraryInfo.LibraryPath\n+        except AttributeError:\n+            enumtype = QLibraryInfo.LibraryLocation\n+\n+        our_names = {member.value for member in qtutils.LibraryPath}\n+        qt_names = set(testutils.enum_members(QLibraryInfo, enumtype))\n+        qt_names.discard(\"ImportsPath\")  # Moved to QmlImportsPath in Qt 6\n+        assert qt_names == our_names\n+\n+\n+def test_extract_enum_val():\n+    value = qtutils.extract_enum_val(Qt.KeyboardModifier.ShiftModifier)\n+    assert value == 0x02000000\n+\n+\n+class TestQObjRepr:\n+\n+    @pytest.mark.parametrize(\"obj\", [QObject(), object(), None])\n+    def test_simple(self, obj):\n+        assert qtutils.qobj_repr(obj) == repr(obj)\n+\n+    def _py_repr(self, obj):\n+        \"\"\"Get the original repr of an object, with &lt;&gt; stripped off.\n+\n+        We do this in code instead of recreating it in tests because of output\n+        differences between PyQt5/PyQt6 and between operating systems.\n+        \"\"\"\n+        r = repr(obj)\n+        if r.startswith(\"&lt;\") and r.endswith(\"&gt;\"):\n+            return r[1:-1]\n+        return r\n+\n+    def test_object_name(self):\n+        obj = QObject()\n+        obj.setObjectName(\"Tux\")\n+        expected = f\"&lt;{self._py_repr(obj)}, objectName='Tux'&gt;\"\n+        assert qtutils.qobj_repr(obj) == expected\n+\n+    def test_class_name(self):\n+        obj = QTimer()  # misc: ignore\n+        hidden = sip.cast(obj, QObject)\n+        expected = f\"&lt;{self._py_repr(hidden)}, className='QTimer'&gt;\"\n+        assert qtutils.qobj_repr(hidden) == expected\n+\n+    def test_both(self):\n+        obj = QTimer()  # misc: ignore\n+        obj.setObjectName(\"Pomodoro\")\n+        hidden = sip.cast(obj, QObject)\n+        expected = f\"&lt;{self._py_repr(hidden)}, objectName='Pomodoro', className='QTimer'&gt;\"\n+        assert qtutils.qobj_repr(hidden) == expected\n+\n+    def test_rich_repr(self):\n+        class RichRepr(QObject):\n+            def __repr__(self):\n+                return \"RichRepr()\"\n+\n+        obj = RichRepr()\n+        assert repr(obj) == \"RichRepr()\"  # sanity check\n+        expected = \"\"\n+        assert qtutils.qobj_repr(obj) == expected\ndiff --git a/tests/unit/utils/test_resources.py b/tests/unit/utils/test_resources.py\nindex 6a11ff588..911f072f1 100644\n--- a/tests/unit/utils/test_resources.py\n+++ b/tests/unit/utils/test_resources.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.resources.\"\"\"\n \ndiff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py\nindex d4eb14208..96bcbcf4c 100644\n--- a/tests/unit/utils/test_standarddir.py\n+++ b/tests/unit/utils/test_standarddir.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.standarddir.\"\"\"\n \n@@ -31,7 +16,7 @@ import subprocess\n from qutebrowser.qt.core import QStandardPaths\n import pytest\n \n-from qutebrowser.utils import standarddir, utils, qtutils, version\n+from qutebrowser.utils import standarddir, utils, version\n \n \n # Use a different application name for tests to make sure we don't change real\n@@ -192,21 +177,6 @@ class TestStandardDir:\n         standarddir._init_dirs()\n         assert func() == str(tmp_path.joinpath(*subdirs))\n \n-    @pytest.mark.linux\n-    @pytest.mark.qt_log_ignore(r'^QStandardPaths: ')\n-    @pytest.mark.skipif(\n-        qtutils.version_check('5.14', compiled=False),\n-        reason=\"Qt 5.14 automatically creates missing runtime dirs\")\n-    def test_linux_invalid_runtimedir(self, monkeypatch, tmpdir):\n-        \"\"\"With invalid XDG_RUNTIME_DIR, fall back to TempLocation.\"\"\"\n-        tmpdir_env = tmpdir / 'temp'\n-        tmpdir_env.ensure(dir=True)\n-        monkeypatch.setenv('XDG_RUNTIME_DIR', str(tmpdir / 'does-not-exist'))\n-        monkeypatch.setenv('TMPDIR', str(tmpdir_env))\n-\n-        standarddir._init_runtime(args=None)\n-        assert standarddir.runtime() == str(tmpdir_env / APPNAME)\n-\n     @pytest.mark.linux\n     @pytest.mark.parametrize('args_basedir', [True, False])\n     def test_flatpak_runtimedir(self, fake_flatpak, monkeypatch, tmp_path,\n@@ -281,7 +251,7 @@ class TestArguments:\n \n     def test_basedir_relative(self, tmpdir):\n         \"\"\"Test --basedir with a relative path.\"\"\"\n-        basedir = (tmpdir / 'basedir')\n+        basedir = tmpdir / 'basedir'\n         basedir.ensure(dir=True)\n         with tmpdir.as_cwd():\n             args = types.SimpleNamespace(basedir='basedir')\ndiff --git a/tests/unit/utils/test_urlmatch.py b/tests/unit/utils/test_urlmatch.py\nindex 9d151a942..85910fa17 100644\n--- a/tests/unit/utils/test_urlmatch.py\n+++ b/tests/unit/utils/test_urlmatch.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2018-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.urlmatch.\n \n@@ -37,7 +22,13 @@ from qutebrowser.qt.core import QUrl\n \n from qutebrowser.utils import urlmatch\n \n-# pylint: disable=line-too-long\n+# FIXME:v4 (lint): disable=line-too-long\n+\n+\n+_INVALID_IP_MESSAGE = (\n+    r'Invalid IPv6 address; source was \".*\"; host = \"\"|'\n+    r\"'.*' does not appear to be an IPv4 or IPv6 address\"  # Python 3.11.4+\n+)\n \n \n @pytest.mark.parametrize('pattern, error', [\n@@ -60,7 +51,11 @@ from qutebrowser.utils import urlmatch\n     pytest.param(\"http://:1234/\", \"Pattern without host\", id='host-port'),\n     pytest.param(\"http://*./\", \"Pattern without host\", id='host-pattern'),\n     ## TEST(ExtensionURLPatternTest, IPv6Patterns)\n-    pytest.param(\"http://[]:8888/*\", \"Pattern without host\", id='host-ipv6'),\n+    pytest.param(\n+        \"http://[]:8888/*\",\n+        \"Pattern without host|'' does not appear to be an IPv4 or IPv6 address\",\n+        id='host-ipv6',\n+    ),\n \n     ### Chromium: kEmptyPath\n     ## TEST(ExtensionURLPatternTest, ParseInvalid)\n@@ -87,19 +82,22 @@ from qutebrowser.utils import urlmatch\n     # Two open brackets (`[[`).\n     pytest.param(\n         \"http://[[2607:f8b0:4005:805::200e]/*\",\n-        r\"\"\"Expected '\\]' to match '\\[' in hostname; source was \"\\[2607:f8b0:4005:805::200e\"; host = \"\"\"\"\",\n+        (\n+            r'''Expected '\\]' to match '\\[' in hostname; source was \"\\[2607:f8b0:4005:805::200e\"; host = \"\"|'''\n+            r\"'\\[2607:f8b0:4005:805::200e' does not appear to be an IPv4 or IPv6 address\"\n+        ),\n         id='host-ipv6-two-open',\n     ),\n     # Too few colons in the last chunk.\n     pytest.param(\n         \"http://[2607:f8b0:4005:805:200e]/*\",\n-        'Invalid IPv6 address; source was \"2607:f8b0:4005:805:200e\"; host = \"\"',\n+        _INVALID_IP_MESSAGE,\n         id='host-ipv6-colons',\n     ),\n     # Non-hex piece.\n     pytest.param(\n         \"http://[2607:f8b0:4005:805:200e:12:bogus]/*\",\n-        'Invalid IPv6 address; source was \"2607:f8b0:4005:805:200e:12:bogus\"; host = \"\"',\n+        _INVALID_IP_MESSAGE,\n         id='host-ipv6-non-hex',\n     ),\n \n@@ -153,33 +151,33 @@ from qutebrowser.utils import urlmatch\n     pytest.param(\"http://[\", \"Invalid IPv6 URL\", id='ipv6-single-open'),\n     pytest.param(\n         \"http://[fc2e::bb88::edac]\",\n-        'Invalid IPv6 address; source was \"fc2e::bb88::edac\"; host = \"\"',\n+        _INVALID_IP_MESSAGE,\n         id='ipv6-double-double',\n     ),\n     pytest.param(\n         \"http://[fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac]\",\n-        'Invalid IPv6 address; source was \"fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac\"; host = \"\"',\n+        _INVALID_IP_MESSAGE,\n         id='ipv6-long-double',\n     ),\n     pytest.param(\n         \"http://[fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac]\",\n-        'Invalid IPv6 address; source was \"fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac\"; host = \"\"',\n+        _INVALID_IP_MESSAGE,\n         id='ipv6-long',\n     ),\n     pytest.param(\n         \"http://[127.0.0.1:fc2e::bb88:edac]\",\n-        r'Invalid IPv6 address; source was \"127\\.0\\.0\\.1:fc2e::bb88:edac',\n+        _INVALID_IP_MESSAGE,\n         id='ipv6-ipv4',\n     ),\n     pytest.param(\"http://[fc2e::bb88\", \"Invalid IPv6 URL\", id='ipv6-trailing'),\n     pytest.param(\n         \"http://[fc2e:bb88:edac]\",\n-        'Invalid IPv6 address; source was \"fc2e:bb88:edac\"; host = \"\"',\n+        _INVALID_IP_MESSAGE,\n         id='ipv6-short',\n     ),\n     pytest.param(\n         \"http://[fc2e:bb88:edac::z]\",\n-        'Invalid IPv6 address; source was \"fc2e:bb88:edac::z\"; host = \"\"',\n+        _INVALID_IP_MESSAGE,\n         id='ipv6-z',\n     ),\n     pytest.param(\n@@ -190,7 +188,7 @@ from qutebrowser.utils import urlmatch\n     pytest.param(\"://\", \"Missing scheme\", id='scheme-naked'),\n ])\n def test_invalid_patterns(pattern, error):\n-    with pytest.raises(urlmatch.ParseError, match=error):\n+    with pytest.raises(urlmatch.ParseError, match=f\"^{error}$\"):\n         urlmatch.UrlPattern(pattern)\n \n # pylint: enable=line-too-long\ndiff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py\nindex 776f6d557..d2eab5928 100644\n--- a/tests/unit/utils/test_urlutils.py\n+++ b/tests/unit/utils/test_urlutils.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.urlutils.\"\"\"\n \n@@ -25,14 +10,14 @@ import dataclasses\n import urllib.parse\n \n from qutebrowser.qt.core import QUrl\n-from qutebrowser.qt.network import QNetworkProxy\n+from qutebrowser.qt.network import QNetworkProxy, QHostInfo\n import pytest\n import hypothesis\n import hypothesis.strategies\n \n from qutebrowser.api import cmdutils\n from qutebrowser.browser.network import pac\n-from qutebrowser.utils import utils, urlutils, usertypes\n+from qutebrowser.utils import utils, urlutils, usertypes, qtutils\n \n \n class FakeDNS:\n@@ -53,7 +38,7 @@ class FakeDNS:\n     @dataclasses.dataclass\n     class FakeDNSAnswer:\n \n-        error: bool\n+        error: QHostInfo.HostInfoError\n \n     def __init__(self):\n         self.used = False\n@@ -68,7 +53,9 @@ class FakeDNS:\n         self.answer = None\n \n     def _get_error(self):\n-        return not self.answer\n+        if self.answer:\n+            return QHostInfo.HostInfoError.NoError\n+        return QHostInfo.HostInfoError.HostNotFound\n \n     def fromname_mock(self, _host):\n         \"\"\"Simple mock for QHostInfo::fromName returning a FakeDNSAnswer.\"\"\"\n@@ -638,6 +625,7 @@ class TestInvalidUrlError:\n     (False, 'http://example.org', 'https://example.org'),  # different scheme\n     (False, 'http://example.org:80', 'http://example.org:8080'),  # different port\n ])\n+@pytest.mark.qt5_only  # https://bugreports.qt.io/browse/QTBUG-80308\n def test_same_domain(are_same, url1, url2):\n     \"\"\"Test same_domain.\"\"\"\n     assert urlutils.same_domain(QUrl(url1), QUrl(url2)) == are_same\n@@ -674,6 +662,18 @@ def test_data_url():\n     assert url == QUrl('data:text/plain;base64,Zm9v')\n \n \n+qurl_idna2003 = pytest.mark.skipif(\n+    qtutils.version_check(\"6.3.0\", compiled=False),\n+    reason=\"Different result with Qt &gt;= 6.3.0: \"\n+    \"https://bugreports.qt.io/browse/QTBUG-85371\"\n+)\n+qurl_uts46 = pytest.mark.xfail(\n+    not qtutils.version_check(\"6.3.0\", compiled=False),\n+    reason=\"Different result with Qt &lt; 6.3.0: \"\n+    \"https://bugreports.qt.io/browse/QTBUG-85371\"\n+)\n+\n+\n @pytest.mark.parametrize('url, expected', [\n     # No IDN\n     (QUrl('http://www.example.com'), 'http://www.example.com'),\n@@ -687,8 +687,16 @@ def test_data_url():\n     (QUrl('http://www.example.xn--p1ai'),\n      '(www.example.xn--p1ai) http://www.example.\u0440\u0444'),\n     # https://bugreports.qt.io/browse/QTBUG-60364\n-    (QUrl('http://www.xn--80ak6aa92e.com'),\n-     'http://www.xn--80ak6aa92e.com'),\n+    pytest.param(\n+        QUrl('http://www.xn--80ak6aa92e.com'),\n+        'http://www.xn--80ak6aa92e.com',\n+        marks=qurl_idna2003,\n+    ),\n+    pytest.param(\n+        QUrl('http://www.xn--80ak6aa92e.com'),\n+        '(www.xn--80ak6aa92e.com) http://www.\u0430\u0440\u0440\u04cf\u0435.com',\n+        marks=qurl_uts46,\n+    ),\n ])\n def test_safe_display_string(url, expected):\n     assert urlutils.safe_display_string(url) == expected\ndiff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py\nindex b4d9703ca..77805c6dc 100644\n--- a/tests/unit/utils/test_utils.py\n+++ b/tests/unit/utils/test_utils.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.utils.\"\"\"\n \n@@ -34,7 +19,7 @@ from qutebrowser.qt.core import QUrl, QRect, QPoint\n from qutebrowser.qt.gui import QClipboard\n import pytest\n import hypothesis\n-from hypothesis import strategies\n+from hypothesis import strategies, settings\n import yaml\n \n import qutebrowser\n@@ -425,7 +410,7 @@ class TestPreventExceptions:\n \n     @utils.prevent_exceptions(42)\n     def func_raising(self):\n-        raise Exception\n+        raise RuntimeError(\"something went wrong\")\n \n     def test_raising(self, caplog):\n         \"\"\"Test with a raising function.\"\"\"\n@@ -434,6 +419,7 @@ class TestPreventExceptions:\n         assert ret == 42\n         expected = 'Error in test_utils.TestPreventExceptions.func_raising'\n         assert caplog.messages == [expected]\n+        assert caplog.records[0].exc_info[1].args[0] == \"something went wrong\"\n \n     @utils.prevent_exceptions(42)\n     def func_not_raising(self):\n@@ -448,7 +434,7 @@ class TestPreventExceptions:\n \n     @utils.prevent_exceptions(42, True)\n     def func_predicate_true(self):\n-        raise Exception\n+        raise RuntimeError(\"its-true\")\n \n     def test_predicate_true(self, caplog):\n         \"\"\"Test with a True predicate.\"\"\"\n@@ -456,15 +442,16 @@ class TestPreventExceptions:\n             ret = self.func_predicate_true()\n         assert ret == 42\n         assert len(caplog.records) == 1\n+        assert caplog.records[0].exc_info[1].args[0] == \"its-true\"\n \n     @utils.prevent_exceptions(42, False)\n     def func_predicate_false(self):\n-        raise Exception\n+        raise RuntimeError(\"its-false\")\n \n     def test_predicate_false(self, caplog):\n         \"\"\"Test with a False predicate.\"\"\"\n         with caplog.at_level(logging.ERROR, 'misc'):\n-            with pytest.raises(Exception):\n+            with pytest.raises(RuntimeError, match=\"its-false\"):\n                 self.func_predicate_false()\n         assert not caplog.records\n \n@@ -546,13 +533,17 @@ class TestIsEnum:\n         assert not utils.is_enum(23)\n \n \n+class SentinalException(Exception):\n+    pass\n+\n+\n class TestRaises:\n \n     \"\"\"Test raises.\"\"\"\n \n     def do_raise(self):\n         \"\"\"Helper function which raises an exception.\"\"\"\n-        raise Exception\n+        raise SentinalException\n \n     def do_nothing(self):\n         \"\"\"Helper function which does nothing.\"\"\"\n@@ -571,15 +562,15 @@ class TestRaises:\n \n     def test_no_args_true(self):\n         \"\"\"Test with no args and an exception which gets raised.\"\"\"\n-        assert utils.raises(Exception, self.do_raise)\n+        assert utils.raises(SentinalException, self.do_raise)\n \n     def test_no_args_false(self):\n         \"\"\"Test with no args and an exception which does not get raised.\"\"\"\n-        assert not utils.raises(Exception, self.do_nothing)\n+        assert not utils.raises(SentinalException, self.do_nothing)\n \n     def test_unrelated_exception(self):\n         \"\"\"Test with an unrelated exception.\"\"\"\n-        with pytest.raises(Exception):\n+        with pytest.raises(SentinalException):\n             utils.raises(ValueError, self.do_raise)\n \n \n@@ -657,6 +648,7 @@ class TestSanitizeFilename:\n         assert utils.sanitize_filename(name, replacement=None) == 'Bad File'\n \n     @hypothesis.given(filename=strategies.text(min_size=100))\n+    @settings(max_examples=10)\n     def test_invariants(self, filename):\n         sanitized = utils.sanitize_filename(filename, shorten=True)\n         assert len(os.fsencode(sanitized)) &lt;= 255 - len(\"(123).download\")\n@@ -767,7 +759,7 @@ class TestOpenFile:\n \n     @pytest.fixture\n     def openurl_mock(self, mocker):\n-        return mocker.patch('PyQt5.QtGui.QDesktopServices.openUrl', spec={},\n+        return mocker.patch('qutebrowser.qt.gui.QDesktopServices.openUrl', spec={},\n                             new_callable=mocker.Mock)\n \n     def test_system_default_application(self, caplog, config_stub,\n@@ -891,7 +883,7 @@ def test_ceil_log_hypothesis(number, base):\n @pytest.mark.parametrize('number, base', [(64, 0), (0, 64), (64, -1),\n                                           (-1, 64), (1, 1)])\n def test_ceil_log_invalid(number, base):\n-    with pytest.raises(Exception):  # ValueError/ZeroDivisionError\n+    with pytest.raises((ValueError, ZeroDivisionError)):\n         math.log(number, base)\n     with pytest.raises(ValueError):\n         utils.ceil_log(number, base)\ndiff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py\nindex 33982da92..5d2863100 100644\n--- a/tests/unit/utils/test_version.py\n+++ b/tests/unit/utils/test_version.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for qutebrowser.utils.version.\"\"\"\n \n@@ -29,10 +14,12 @@ import logging\n import textwrap\n import datetime\n import dataclasses\n+import importlib.metadata\n \n import pytest\n import hypothesis\n import hypothesis.strategies\n+from qutebrowser.qt import machinery\n from qutebrowser.qt.core import PYQT_VERSION_STR\n \n import qutebrowser\n@@ -657,8 +644,8 @@ class TestModuleVersions:\n         assert version._module_versions() == expected\n \n     @pytest.mark.parametrize('module, idx, expected', [\n-        ('colorama', 1, 'colorama: no'),\n-        ('adblock', 5, 'adblock: no'),\n+        ('colorama', 0, 'colorama: no'),\n+        ('adblock', 4, 'adblock: no'),\n     ])\n     def test_missing_module(self, module, idx, expected, import_fake):\n         \"\"\"Test with a module missing.\n@@ -706,11 +693,11 @@ class TestModuleVersions:\n         assert not mod_info.is_usable()\n \n         expected = f\"adblock: {fake_version} (&lt; {mod_info.min_version}, outdated)\"\n-        assert version._module_versions()[5] == expected\n+        assert version._module_versions()[4] == expected\n \n     @pytest.mark.parametrize('attribute, expected_modules', [\n         ('VERSION', ['colorama']),\n-        ('SIP_VERSION_STR', ['sip']),\n+        ('SIP_VERSION_STR', ['PyQt5.sip', 'PyQt6.sip']),\n         (None, []),\n     ])\n     def test_version_attribute(self, attribute, expected_modules, import_fake):\n@@ -898,9 +885,7 @@ class TestPDFJSVersion:\n \n     def test_real_file(self, data_tmpdir):\n         \"\"\"Test against the real file if pdfjs was found.\"\"\"\n-        try:\n-            pdfjs.get_pdfjs_res_and_path('build/pdf.js')\n-        except pdfjs.PDFJSNotFound:\n+        if not pdfjs.is_available():\n             pytest.skip(\"No pdfjs found\")\n         ver = version._pdfjs_version()\n         assert ver.split()[0] not in ['no', 'unknown'], ver\n@@ -914,21 +899,45 @@ class TestWebEngineVersions:\n                 webengine=utils.VersionNumber(5, 15, 2),\n                 chromium=None,\n                 source='UA'),\n-            \"QtWebEngine 5.15.2\",\n+            (\n+                \"QtWebEngine 5.15.2\\n\"\n+                \"  (source: UA)\"\n+            ),\n         ),\n         (\n             version.WebEngineVersions(\n                 webengine=utils.VersionNumber(5, 15, 2),\n                 chromium='87.0.4280.144',\n                 source='UA'),\n-            \"QtWebEngine 5.15.2, based on Chromium 87.0.4280.144\",\n+            (\n+                \"QtWebEngine 5.15.2\\n\"\n+                \"  based on Chromium 87.0.4280.144\\n\"\n+                \"  (source: UA)\"\n+            ),\n         ),\n         (\n             version.WebEngineVersions(\n                 webengine=utils.VersionNumber(5, 15, 2),\n                 chromium='87.0.4280.144',\n                 source='faked'),\n-            \"QtWebEngine 5.15.2, based on Chromium 87.0.4280.144 (from faked)\",\n+            (\n+                \"QtWebEngine 5.15.2\\n\"\n+                \"  based on Chromium 87.0.4280.144\\n\"\n+                \"  (source: faked)\"\n+            ),\n+        ),\n+        (\n+            version.WebEngineVersions(\n+                webengine=utils.VersionNumber(5, 15, 2),\n+                chromium='87.0.4280.144',\n+                chromium_security='9000.1',\n+                source='faked'),\n+            (\n+                \"QtWebEngine 5.15.2\\n\"\n+                \"  based on Chromium 87.0.4280.144\\n\"\n+                \"  with security patches up to 9000.1 (plus any distribution patches)\\n\"\n+                \"  (source: faked)\"\n+            ),\n         ),\n     ])\n     def test_str(self, version, expected):\n@@ -965,6 +974,7 @@ class TestWebEngineVersions:\n         expected = version.WebEngineVersions(\n             webengine=utils.VersionNumber(5, 15, 2),\n             chromium='83.0.4103.122',\n+            chromium_security='86.0.4240.183',\n             source='UA',\n         )\n         assert version.WebEngineVersions.from_ua(ua) == expected\n@@ -974,22 +984,27 @@ class TestWebEngineVersions:\n         expected = version.WebEngineVersions(\n             webengine=utils.VersionNumber(5, 15, 2),\n             chromium='83.0.4103.122',\n+            chromium_security='86.0.4240.183',\n             source='ELF',\n         )\n         assert version.WebEngineVersions.from_elf(elf_version) == expected\n \n-    @pytest.mark.parametrize('pyqt_version, chromium_version', [\n-        ('5.12.10', '69.0.3497.128'),\n-        ('5.14.2', '77.0.3865.129'),\n-        ('5.15.1', '80.0.3987.163'),\n-        ('5.15.2', '83.0.4103.122'),\n-        ('5.15.3', '87.0.4280.144'),\n-        ('5.15.4', '87.0.4280.144'),\n-        ('5.15.5', '87.0.4280.144'),\n+    @pytest.mark.parametrize('pyqt_version, chromium_version, security_version', [\n+        ('5.15.2', '83.0.4103.122', '86.0.4240.183'),\n+        ('5.15.3', '87.0.4280.144', '88.0.4324.150'),\n+        ('5.15.4', '87.0.4280.144', None),\n+        ('5.15.5', '87.0.4280.144', None),\n+        ('5.15.6', '87.0.4280.144', None),\n+        ('5.15.7', '87.0.4280.144', '94.0.4606.61'),\n+        ('6.2.0', '90.0.4430.228', '93.0.4577.63'),\n+        ('6.2.99', '90.0.4430.228', None),\n+        ('6.3.0', '94.0.4606.126', '99.0.4844.84'),\n+        ('6.99.0', None, None),\n     ])\n-    def test_from_pyqt(self, freezer, pyqt_version, chromium_version):\n-        if freezer and pyqt_version in ['5.15.3', '5.15.4', '5.15.5']:\n+    def test_from_pyqt(self, freezer, pyqt_version, chromium_version, security_version):\n+        if freezer and utils.VersionNumber(5, 15, 3) &lt;= utils.VersionNumber.parse(pyqt_version) &lt; utils.VersionNumber(6):\n             chromium_version = '83.0.4103.122'\n+            security_version = '86.0.4240.183'\n             expected_pyqt_version = '5.15.2'\n         else:\n             expected_pyqt_version = pyqt_version\n@@ -997,25 +1012,37 @@ class TestWebEngineVersions:\n         expected = version.WebEngineVersions(\n             webengine=utils.VersionNumber.parse(expected_pyqt_version),\n             chromium=chromium_version,\n+            chromium_security=security_version,\n             source='PyQt',\n         )\n         assert version.WebEngineVersions.from_pyqt(pyqt_version) == expected\n \n     def test_real_chromium_version(self, qapp):\n         \"\"\"Compare the inferred Chromium version with the real one.\"\"\"\n+        try:\n+            # pylint: disable=unused-import\n+            from qutebrowser.qt.webenginecore import (\n+                qWebEngineVersion,\n+                qWebEngineChromiumVersion,\n+            )\n+        except ImportError:\n+            pass\n+        else:\n+            pytest.skip(\"API available to get the real version\")\n+\n         pyqt_webengine_version = version._get_pyqt_webengine_qt_version()\n         if pyqt_webengine_version is None:\n             if '.dev' in PYQT_VERSION_STR:\n-                pytest.skip(\"dev version of PyQt5\")\n+                pytest.skip(\"dev version of PyQt\")\n \n             try:\n                 from qutebrowser.qt.webenginecore import (\n                     PYQT_WEBENGINE_VERSION_STR, PYQT_WEBENGINE_VERSION)\n             except ImportError as e:\n-                # QtWebKit or QtWebEngine &lt; 5.13\n+                # QtWebKit\n                 pytest.skip(str(e))\n \n-            if PYQT_WEBENGINE_VERSION &gt;= 0x050F02:\n+            if 0x060000 &gt; PYQT_WEBENGINE_VERSION &gt;= 0x050F02:\n                 # Starting with Qt 5.15.2, we can only do bad guessing anyways...\n                 pytest.skip(\"Could be QtWebEngine 5.15.2 or 5.15.3\")\n \n@@ -1029,6 +1056,39 @@ class TestWebEngineVersions:\n \n         assert inferred == real\n \n+    def test_real_chromium_security_version(self, qapp):\n+        \"\"\"Check the API for reading the chromium security patch version.\"\"\"\n+        try:\n+            from qutebrowser.qt.webenginecore import (\n+                qWebEngineChromiumVersion,\n+                qWebEngineChromiumSecurityPatchVersion,\n+            )\n+        except ImportError:\n+            pytest.skip(\"Requires QtWebEngine 6.3+\")\n+\n+        base = utils.VersionNumber.parse(qWebEngineChromiumVersion())\n+        security = utils.VersionNumber.parse(qWebEngineChromiumSecurityPatchVersion())\n+        assert security &gt;= base\n+\n+    def test_chromium_security_version_dict(self, qapp):\n+        \"\"\"Check if we infer the QtWebEngine security version properly.\n+\n+        Note this test mostly tests that our overview in version.py (also\n+        intended for human readers) is accurate. The code we call here is never\n+        going to be called in real-life situations, as the API is available.\n+        \"\"\"\n+        try:\n+            from qutebrowser.qt.webenginecore import (\n+                qWebEngineVersion,\n+                qWebEngineChromiumSecurityPatchVersion,\n+            )\n+        except ImportError:\n+            pytest.skip(\"Requires QtWebEngine 6.3+\")\n+\n+        inferred = version.WebEngineVersions.from_webengine(\n+            qWebEngineVersion(), source=\"API\")\n+        assert inferred.chromium_security == qWebEngineChromiumSecurityPatchVersion()\n+\n \n class FakeQSslSocket:\n \n@@ -1063,18 +1123,18 @@ class TestChromiumVersion:\n \n     @pytest.fixture(autouse=True)\n     def clear_parsed_ua(self, monkeypatch):\n-        pytest.importorskip('PyQt5.QtWebEngineWidgets')\n+        pytest.importorskip('qutebrowser.qt.webenginewidgets')\n         if webenginesettings is not None:\n             # Not available with QtWebKit\n             monkeypatch.setattr(webenginesettings, 'parsed_user_agent', None)\n \n-    def test_fake_ua(self, monkeypatch, caplog):\n+    def test_fake_ua(self, monkeypatch, caplog, patch_no_api):\n         ver = '77.0.3865.98'\n         webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format(ver))\n \n         assert version.qtwebengine_versions().chromium == ver\n \n-    def test_prefers_saved_user_agent(self, monkeypatch):\n+    def test_prefers_saved_user_agent(self, monkeypatch, patch_no_api):\n         webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format('87'))\n \n         class FakeProfile:\n@@ -1090,64 +1150,53 @@ class TestChromiumVersion:\n \n     def test_avoided(self, monkeypatch):\n         versions = version.qtwebengine_versions(avoid_init=True)\n-        assert versions.source in ['ELF', 'importlib', 'PyQt', 'Qt']\n+        assert versions.source in ['api', 'ELF', 'importlib', 'PyQt', 'Qt']\n+\n+    @pytest.fixture\n+    def patch_no_api(self, monkeypatch):\n+        \"\"\"Simulate no PyQt API for getting its version.\"\"\"\n+        monkeypatch.delattr(\n+            qutebrowser.qt.webenginecore,\n+            \"qWebEngineVersion\",\n+            raising=False,\n+        )\n \n     @pytest.fixture\n     def patch_elf_fail(self, monkeypatch):\n         \"\"\"Simulate parsing the version from ELF to fail.\"\"\"\n         monkeypatch.setattr(elf, 'parse_webenginecore', lambda: None)\n \n-    @pytest.fixture\n-    def patch_old_pyqt(self, monkeypatch):\n-        \"\"\"Simulate an old PyQt without PYQT_WEBENGINE_VERSION_STR.\"\"\"\n-        monkeypatch.setattr(version, 'PYQT_WEBENGINE_VERSION_STR', None)\n-\n-    @pytest.fixture\n-    def patch_no_importlib(self, monkeypatch, stubs):\n-        \"\"\"Simulate missing importlib modules.\"\"\"\n-        import_fake = stubs.ImportFake({\n-            'importlib_metadata': False,\n-            'importlib.metadata': False,\n-        }, monkeypatch)\n-        import_fake.patch()\n-\n     @pytest.fixture\n     def importlib_patcher(self, monkeypatch):\n         \"\"\"Patch the importlib module.\"\"\"\n-        def _patch(*, qt, qt5):\n-            try:\n-                import importlib.metadata as importlib_metadata\n-            except ImportError:\n-                importlib_metadata = pytest.importorskip(\"importlib_metadata\")\n-\n+        def _patch(*, qt, qt5, qt6):\n             def _fake_version(name):\n                 if name == 'PyQtWebEngine-Qt':\n                     outcome = qt\n                 elif name == 'PyQtWebEngine-Qt5':\n                     outcome = qt5\n+                elif name == 'PyQt6-WebEngine-Qt6':\n+                    outcome = qt6\n                 else:\n-                    raise utils.Unreachable(outcome)\n+                    raise utils.Unreachable(name)\n \n                 if outcome is None:\n-                    raise importlib_metadata.PackageNotFoundError(name)\n+                    raise importlib.metadata.PackageNotFoundError(name)\n                 return outcome\n \n-            monkeypatch.setattr(importlib_metadata, 'version', _fake_version)\n+            monkeypatch.setattr(importlib.metadata, 'version', _fake_version)\n \n         return _patch\n \n     @pytest.fixture\n     def patch_importlib_no_package(self, importlib_patcher):\n-        \"\"\"Simulate importlib not finding PyQtWebEngine-Qt[5].\"\"\"\n-        importlib_patcher(qt=None, qt5=None)\n+        \"\"\"Simulate importlib not finding PyQtWebEngine Qt packages.\"\"\"\n+        importlib_patcher(qt=None, qt5=None, qt6=None)\n \n     @pytest.mark.parametrize('patches, sources', [\n-        (['elf_fail'], ['importlib', 'PyQt', 'Qt']),\n-        (['elf_fail', 'old_pyqt'], ['importlib', 'Qt']),\n-        (['elf_fail', 'no_importlib'], ['PyQt', 'Qt']),\n-        (['elf_fail', 'no_importlib', 'old_pyqt'], ['Qt']),\n-        (['elf_fail', 'importlib_no_package'], ['PyQt', 'Qt']),\n-        (['elf_fail', 'importlib_no_package', 'old_pyqt'], ['Qt']),\n+        (['no_api'], ['ELF', 'importlib', 'PyQt', 'Qt']),\n+        (['no_api', 'elf_fail'], ['importlib', 'PyQt', 'Qt']),\n+        (['no_api', 'elf_fail', 'importlib_no_package'], ['PyQt', 'Qt']),\n     ], ids=','.join)\n     def test_simulated(self, request, patches, sources):\n         \"\"\"Test various simulated error conditions.\n@@ -1163,17 +1212,41 @@ class TestChromiumVersion:\n         versions = version.qtwebengine_versions(avoid_init=True)\n         assert versions.source in sources\n \n-    @pytest.mark.parametrize('qt, qt5, expected', [\n-        (None, '5.15.4', utils.VersionNumber(5, 15, 4)),\n-        ('5.15.3', None, utils.VersionNumber(5, 15, 3)),\n-        ('5.15.3', '5.15.4', utils.VersionNumber(5, 15, 4)),  # -Qt5 takes precedence\n+    @pytest.mark.parametrize('qt, qt5, qt6, expected', [\n+        pytest.param(\n+            None, None, '6.3.0',\n+            utils.VersionNumber(6, 3),\n+            marks=pytest.mark.qt6_only,\n+        ),\n+        pytest.param(\n+            '5.15.3', '5.15.4', '6.3.0',\n+            utils.VersionNumber(6, 3),\n+            marks=pytest.mark.qt6_only,\n+        ),\n+\n+        pytest.param(\n+            None, '5.15.4', None,\n+            utils.VersionNumber(5, 15, 4),\n+            marks=pytest.mark.qt5_only,\n+        ),\n+        pytest.param(\n+            '5.15.3', None, None,\n+            utils.VersionNumber(5, 15, 3),\n+            marks=pytest.mark.qt5_only,\n+        ),\n+        # -Qt5 takes precedence\n+        pytest.param(\n+            '5.15.3', '5.15.4', None,\n+            utils.VersionNumber(5, 15, 4),\n+            marks=pytest.mark.qt5_only,\n+        ),\n     ])\n-    def test_importlib(self, qt, qt5, expected, patch_elf_fail, importlib_patcher):\n+    def test_importlib(self, qt, qt5, qt6, expected, patch_elf_fail, patch_no_api, importlib_patcher):\n         \"\"\"Test the importlib version logic with different Qt packages.\n \n         With PyQtWebEngine 5.15.4, PyQtWebEngine-Qt was renamed to PyQtWebEngine-Qt5.\n         \"\"\"\n-        importlib_patcher(qt=qt, qt5=qt5)\n+        importlib_patcher(qt=qt, qt5=qt5, qt6=qt6)\n         versions = version.qtwebengine_versions(avoid_init=True)\n         assert versions.source == 'importlib'\n         assert versions.webengine == expected\n@@ -1182,9 +1255,10 @@ class TestChromiumVersion:\n         utils.VersionNumber(5, 12, 10),\n         utils.VersionNumber(5, 15, 3),\n     ])\n-    def test_override(self, monkeypatch, override):\n+    @pytest.mark.parametrize('avoid_init', [True, False])\n+    def test_override(self, monkeypatch, override, avoid_init):\n         monkeypatch.setenv('QUTE_QTWEBENGINE_VERSION_OVERRIDE', str(override))\n-        versions = version.qtwebengine_versions(avoid_init=True)\n+        versions = version.qtwebengine_versions(avoid_init=avoid_init)\n         assert versions.source == 'override'\n         assert versions.webengine == override\n \n@@ -1241,6 +1315,10 @@ def test_version_info(params, stubs, monkeypatch, config_stub):\n         'sql.version': lambda: 'SQLITE VERSION',\n         '_uptime': lambda: datetime.timedelta(hours=1, minutes=23, seconds=45),\n         'config.instance.yaml_loaded': params.autoconfig_loaded,\n+        'machinery.INFO': machinery.SelectionInfo(\n+            wrapper=\"QT WRAPPER\",\n+            reason=machinery.SelectionReason.fake\n+        ),\n     }\n \n     version.opengl_info.cache_clear()\n@@ -1281,7 +1359,7 @@ def test_version_info(params, stubs, monkeypatch, config_stub):\n     else:\n         monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False)\n         patches['objects.backend'] = usertypes.Backend.QtWebEngine\n-        substitutions['backend'] = 'QtWebEngine 1.2.3 (from faked)'\n+        substitutions['backend'] = 'QtWebEngine 1.2.3\\n  (source: faked)'\n \n     if params.known_distribution:\n         patches['distribution'] = lambda: version.DistributionInfo(\n@@ -1312,6 +1390,8 @@ def test_version_info(params, stubs, monkeypatch, config_stub):\n         PYTHON IMPLEMENTATION: PYTHON VERSION\n         PyQt: PYQT VERSION\n \n+        Qt wrapper: QT WRAPPER (via fake)\n+\n         MODULE VERSION 1\n         MODULE VERSION 2\n         pdf.js: PDFJS VERSION\n@@ -1345,7 +1425,7 @@ class TestOpenGLInfo:\n \n     def test_func(self, qapp):\n         \"\"\"Simply call version.opengl_info() and see if it doesn't crash.\"\"\"\n-        pytest.importorskip(\"PyQt5.QtOpenGL\")\n+        pytest.importorskip(\"qutebrowser.qt.opengl\")\n         version.opengl_info()\n \n     def test_func_fake(self, qapp, monkeypatch):\ndiff --git a/tests/unit/utils/usertypes/test_misc.py b/tests/unit/utils/usertypes/test_misc.py\nindex 216ec70b2..a94239bb2 100644\n--- a/tests/unit/utils/usertypes/test_misc.py\n+++ b/tests/unit/utils/usertypes/test_misc.py\n@@ -1,22 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2016-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n-\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n from qutebrowser.utils import usertypes\n \ndiff --git a/tests/unit/utils/usertypes/test_neighborlist.py b/tests/unit/utils/usertypes/test_neighborlist.py\nindex 3abaeb3a7..7bc1e8ef2 100644\n--- a/tests/unit/utils/usertypes/test_neighborlist.py\n+++ b/tests/unit/utils/usertypes/test_neighborlist.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2014-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for the NeighborList class.\"\"\"\n \ndiff --git a/tests/unit/utils/usertypes/test_question.py b/tests/unit/utils/usertypes/test_question.py\nindex 5e3731109..6915c8539 100644\n--- a/tests/unit/utils/usertypes/test_question.py\n+++ b/tests/unit/utils/usertypes/test_question.py\n@@ -1,21 +1,6 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n-#\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for usertypes.Question.\"\"\"\n \ndiff --git a/tests/unit/utils/usertypes/test_timer.py b/tests/unit/utils/usertypes/test_timer.py\nindex 97c116f01..6aabc8c04 100644\n--- a/tests/unit/utils/usertypes/test_timer.py\n+++ b/tests/unit/utils/usertypes/test_timer.py\n@@ -1,24 +1,12 @@\n-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:\n-\n-# Copyright 2015-2021 Florian Bruhin (The Compiler) \n-#\n-# This file is part of qutebrowser.\n-#\n-# qutebrowser is free software: you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License as published by\n-# the Free Software Foundation, either version 3 of the License, or\n-# (at your option) any later version.\n+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) \n #\n-# qutebrowser is distributed in the hope that it will be useful,\n-# but WITHOUT ANY WARRANTY; without even the implied warranty of\n-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n-# GNU General Public License for more details.\n-#\n-# You should have received a copy of the GNU General Public License\n-# along with qutebrowser.  If not, see .\n+# SPDX-License-Identifier: GPL-3.0-or-later\n \n \"\"\"Tests for Timer.\"\"\"\n \n+import logging\n+import fnmatch\n+\n import pytest\n from qutebrowser.qt.core import QObject\n \n@@ -80,3 +68,63 @@ def test_timeout_set_interval(qtbot):\n     with qtbot.wait_signal(t.timeout, timeout=3000):\n         t.setInterval(200)\n         t.start()\n+\n+\n+@pytest.mark.parametrize(\n+    \"elapsed_ms, expected\",\n+    [\n+        (0, False),\n+        (1, False),\n+        (600, True),\n+        (999, True),\n+        (1000, True),\n+    ],\n+)\n+def test_early_timeout_check(qtbot, mocker, elapsed_ms, expected):\n+    time_mock = mocker.patch(\"time.monotonic\", autospec=True)\n+\n+    t = usertypes.Timer()\n+    t.setInterval(1000)  # anything long enough to not actually fire\n+    time_mock.return_value = 0  # assigned to _start_time in start()\n+    t.start()\n+    time_mock.return_value = elapsed_ms / 1000  # used for `elapsed`\n+\n+    assert t.check_timeout_validity() is expected\n+\n+    t.stop()\n+\n+\n+def test_early_timeout_handler(qtbot, mocker, caplog):\n+    time_mock = mocker.patch(\"time.monotonic\", autospec=True)\n+\n+    t = usertypes.Timer(name=\"t\")\n+    t.setInterval(3)\n+    t.setSingleShot(True)\n+    time_mock.return_value = 0\n+    with caplog.at_level(logging.WARNING):\n+        with qtbot.wait_signal(t.timeout, timeout=10):\n+            t.start()\n+            time_mock.return_value = 1 / 1000\n+\n+        assert len(caplog.messages) == 1\n+        assert fnmatch.fnmatch(\n+            caplog.messages[-1],\n+            \"Timer t (id *) triggered too early: interval 3 but only 0.001s passed\",\n+        )\n+\n+\n+def test_early_manual_fire(qtbot, mocker, caplog):\n+    \"\"\"Same as above but start() never gets called.\"\"\"\n+    time_mock = mocker.patch(\"time.monotonic\", autospec=True)\n+\n+    t = usertypes.Timer(name=\"t\")\n+    t.setInterval(3)\n+    t.setSingleShot(True)\n+    time_mock.return_value = 0\n+    with caplog.at_level(logging.WARNING):\n+        with qtbot.wait_signal(t.timeout, timeout=10):\n+            t.timeout.emit()\n+            time_mock.return_value = 1 / 1000\n+\n+        assert len(caplog.messages) == 0\n+        assert t.check_timeout_validity()\ndiff --git a/tox.ini b/tox.ini\nindex 8d33750f3..f7454740b 100644\n--- a/tox.ini\n+++ b/tox.ini\n@@ -4,46 +4,77 @@\n # and then run \"tox\" from this directory.\n \n [tox]\n-envlist = py38-pyqt515-cov,mypy,misc,vulture,flake8,pylint,pyroma,check-manifest,eslint,yamllint,actionlint\n+envlist = py38-pyqt515-cov,mypy-pyqt5,misc,vulture,flake8,pylint,pyroma,check-manifest,eslint,yamllint,actionlint\n distshare = {toxworkdir}\n skipsdist = true\n-minversion = 3.15\n+minversion = 3.20\n \n [testenv]\n setenv =\n-    PYTEST_QT_API=pyqt5\n-    pyqt{,512,513,514,515,5150}: LINK_PYQT_SKIP=true\n+    PYTEST_QT_API=pyqt6\n+    QUTE_QT_WRAPPER=PyQt6\n+    pyqt{515,5152}: PYTEST_QT_API=pyqt5\n+    pyqt{515,5152}: QUTE_QT_WRAPPER=PyQt5\n     cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report=\n-passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND FORCE_COLOR DBUS_SESSION_BUS_ADDRESS\n+passenv =\n+    PYTHON\n+    DISPLAY\n+    XAUTHORITY\n+    HOME\n+    USERNAME\n+    USER\n+    CI\n+    XDG_*\n+    QUTE_*\n+    DOCKER\n+    QT_QUICK_BACKEND\n+    FORCE_COLOR\n+    DBUS_SESSION_BUS_ADDRESS\n basepython =\n     py: {env:PYTHON:python3}\n     py3: {env:PYTHON:python3}\n-    py37: {env:PYTHON:python3.7}\n     py38: {env:PYTHON:python3.8}\n     py39: {env:PYTHON:python3.9}\n     py310: {env:PYTHON:python3.10}\n     py311: {env:PYTHON:python3.11}\n+    py312: {env:PYTHON:python3.12}\n+    py313: {env:PYTHON:python3.13}\n deps =\n     -r{toxinidir}/requirements.txt\n     -r{toxinidir}/misc/requirements/requirements-tests.txt\n+    -r{toxinidir}/misc/requirements/requirements-docs.txt\n     pyqt: -r{toxinidir}/misc/requirements/requirements-pyqt.txt\n-    pyqt512: -r{toxinidir}/misc/requirements/requirements-pyqt-5.12.txt\n-    pyqt513: -r{toxinidir}/misc/requirements/requirements-pyqt-5.13.txt\n-    pyqt514: -r{toxinidir}/misc/requirements/requirements-pyqt-5.14.txt\n     pyqt515: -r{toxinidir}/misc/requirements/requirements-pyqt-5.15.txt\n-    pyqt5150: -r{toxinidir}/misc/requirements/requirements-pyqt-5.15.0.txt\n+    pyqt5152: -r{toxinidir}/misc/requirements/requirements-pyqt-5.15.2.txt\n+    pyqt62: -r{toxinidir}/misc/requirements/requirements-pyqt-6.2.txt\n+    pyqt63: -r{toxinidir}/misc/requirements/requirements-pyqt-6.3.txt\n+    pyqt64: -r{toxinidir}/misc/requirements/requirements-pyqt-6.4.txt\n+    pyqt65: -r{toxinidir}/misc/requirements/requirements-pyqt-6.5.txt\n+    pyqt66: -r{toxinidir}/misc/requirements/requirements-pyqt-6.6.txt\n+    pyqt67: -r{toxinidir}/misc/requirements/requirements-pyqt-6.7.txt\n+commands_pre =\n+    py313: pip install -U --pre typing-extensions==4.12.0rc1  # FIXME remove once released\n commands =\n-    {envpython} scripts/link_pyqt.py --tox {envdir}\n+    !pyqt-!pyqt515-!pyqt5152-!pyqt62-!pyqt63-!pyqt64-!pyqt65-!pyqt66-!pyqt67: {envpython} scripts/link_pyqt.py --tox {envdir}\n     {envpython} -bb -m pytest {posargs:tests}\n     cov: {envpython} scripts/dev/check_coverage.py {posargs}\n \n-[testenv:bleeding]\n-basepython = {env:PYTHON:python3}\n+[testenv:py-qt5]\n setenv =\n     PYTEST_QT_API=pyqt5\n+    QUTE_QT_WRAPPER=PyQt5\n+\n+[testenv:bleeding{,-qt5}]\n+basepython = {env:PYTHON:python3}\n+# Override default PyQt6 from [testenv]\n+setenv =\n+    qt5: PYTEST_QT_API=pyqt5\n+    qt5: QUTE_QT_WRAPPER=PyQt5\n pip_pre = true\n deps = -r{toxinidir}/misc/requirements/requirements-tests-bleeding.txt\n-commands_pre = pip install --index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt5 PyQtWebEngine\n+commands_pre =\n+    qt5: pip install --extra-index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade --only-binary PyQt5,PyQtWebEngine PyQt5 PyQtWebEngine PyQt5-Qt5 PyQtWebEngine-Qt5 PyQt5-sip\n+    !qt5: pip install --extra-index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade --only-binary PyQt6,PyQt6-WebEngine PyQt6 PyQt6-WebEngine PyQt6-Qt6 PyQt6-WebEngine-Qt6 PyQt6-sip\n commands = {envpython} -bb -m pytest {posargs:tests}\n \n # other envs\n@@ -72,7 +103,9 @@ basepython = {env:PYTHON:python3}\n deps =\n     -r{toxinidir}/requirements.txt\n     -r{toxinidir}/misc/requirements/requirements-vulture.txt\n-setenv = PYTHONPATH={toxinidir}\n+setenv =\n+    {[testenv]setenv}\n+    {[testenv:vulture]setenv}\n commands =\n     {envpython} scripts/link_pyqt.py --tox {envdir}\n     {[testenv:vulture]commands}\n@@ -86,6 +119,7 @@ deps =\n     -r{toxinidir}/misc/requirements/requirements-tests.txt\n     -r{toxinidir}/misc/requirements/requirements-pylint.txt\n     -r{toxinidir}/misc/requirements/requirements-pyqt.txt\n+    -r{toxinidir}/misc/requirements/requirements-pyqt-5.txt\n commands =\n     {envpython} -m pylint scripts qutebrowser --output-format=colorized --reports=no {posargs}\n     {envpython} scripts/dev/run_pylint_on_tests.py {toxinidir} --output-format=colorized --reports=no {posargs}\n@@ -142,23 +176,31 @@ commands =\n \n [testenv:docs]\n basepython = {env:PYTHON:python3}\n-whitelist_externals = git\n-passenv = CI GITHUB_REF\n+passenv =\n+    CI\n+    GITHUB_REF\n deps =\n     -r{toxinidir}/requirements.txt\n+    -r{toxinidir}/misc/requirements/requirements-docs.txt\n     -r{toxinidir}/misc/requirements/requirements-pyqt.txt\n commands =\n     {envpython} scripts/dev/src2asciidoc.py {posargs}\n     {envpython} scripts/dev/check_doc_changes.py {posargs}\n     {envpython} scripts/asciidoc2html.py {posargs}\n \n-[testenv:pyinstaller-{64,32}]\n+[testenv:pyinstaller{,-qt5}]\n basepython = {env:PYTHON:python3}\n-passenv = APPDATA HOME PYINSTALLER_DEBUG\n+passenv =\n+    APPDATA\n+    HOME\n+    PYINSTALLER_DEBUG\n+setenv =\n+    qt5: PYINSTALLER_QT5=true\n deps =\n     -r{toxinidir}/requirements.txt\n     -r{toxinidir}/misc/requirements/requirements-pyinstaller.txt\n-    -r{toxinidir}/misc/requirements/requirements-pyqt.txt\n+    !qt5: -r{toxinidir}/misc/requirements/requirements-pyqt-6.txt\n+    qt5: -r{toxinidir}/misc/requirements/requirements-pyqt-5.txt\n commands =\n     {envbindir}/pyinstaller --noconfirm misc/qutebrowser.spec\n \n@@ -166,26 +208,33 @@ commands =\n basepython = python3\n deps =\n passenv = TERM\n-whitelist_externals = eslint\n+allowlist_externals = eslint\n changedir = {toxinidir}/qutebrowser/javascript\n commands = eslint --report-unused-disable-directives .\n \n [testenv:shellcheck]\n basepython = python3\n deps =\n-whitelist_externals = bash\n+allowlist_externals = bash\n commands = bash scripts/dev/run_shellcheck.sh {posargs}\n \n-[testenv:mypy]\n+[testenv:mypy-{pyqt5,pyqt6}]\n basepython = {env:PYTHON:python3}\n-passenv = TERM MYPY_FORCE_TERMINAL_WIDTH\n+passenv =\n+    TERM\n+    MYPY_FORCE_TERMINAL_WIDTH\n+setenv =\n+    # See qutebrowser/qt/machinery.py\n+    pyqt6: QUTE_CONSTANTS_ARGS=--always-true=USE_PYQT6 --always-false=USE_PYQT5 --always-false=USE_PYSIDE6 --always-false=IS_QT5 --always-true=IS_QT6 --always-true=IS_PYQT --always-false=IS_PYSIDE\n+    pyqt5: QUTE_CONSTANTS_ARGS=--always-false=USE_PYQT6 --always-true=USE_PYQT5 --always-false=USE_PYSIDE6 --always-true=IS_QT5 --always-false=IS_QT6 --always-true=IS_PYQT --always-false=IS_PYSIDE\n deps =\n     -r{toxinidir}/requirements.txt\n     -r{toxinidir}/misc/requirements/requirements-dev.txt\n     -r{toxinidir}/misc/requirements/requirements-tests.txt\n-    -r{toxinidir}/misc/requirements/requirements-mypy.txt\n+    -r{toxinidir}/misc/requirements/requirements-mypy.txt  # includes PyQt5-stubs\n+    -r{toxinidir}/misc/requirements/requirements-pyqt-6.txt\n commands =\n-    {envpython} -m mypy qutebrowser {posargs}\n+    {envpython} -m mypy {env:QUTE_CONSTANTS_ARGS} qutebrowser {posargs}\n \n [testenv:yamllint]\n basepython = {env:PYTHON:python3}\n@@ -196,17 +245,18 @@ commands =\n [testenv:actionlint]\n basepython = python3\n deps =\n-whitelist_externals = actionlint\n+allowlist_externals = actionlint\n commands =\n     actionlint\n \n-[testenv:mypy-diff]\n+[testenv:mypy-{pyqt5,pyqt6}-diff]\n basepython = {env:PYTHON:python3}\n-passenv = {[testenv:mypy]passenv}\n-deps = {[testenv:mypy]deps}\n+passenv = {[testenv:mypy-pyqt6]passenv}\n+deps = {[testenv:mypy-pyqt6]deps}\n+setenv = {[testenv:mypy-pyqt6]setenv}\n commands =\n-    {envpython} -m mypy --cobertura-xml-report {envtmpdir} qutebrowser tests {posargs}\n-    {envdir}/bin/diff-cover --fail-under=100 --compare-branch={env:DIFF_BRANCH:origin/{env:GITHUB_BASE_REF:master}} {envtmpdir}/cobertura.xml\n+    {envpython} -m mypy --cobertura-xml-report {envtmpdir} {env:QUTE_CONSTANTS_ARGS} qutebrowser tests {posargs}\n+    {envdir}/bin/diff-cover --fail-under=100 --compare-branch={env:DIFF_BRANCH:origin/{env:GITHUB_BASE_REF:main}} {envtmpdir}/cobertura.xml\n \n [testenv:sphinx]\n basepython = {env:PYTHON:python3}\n@@ -219,15 +269,38 @@ deps =\n commands =\n     {envpython} -m sphinx -jauto -W --color {posargs} {toxinidir}/doc/extapi/ {toxinidir}/doc/extapi/_build/\n \n-[testenv:build-release]\n+[testenv:update-version]\n+basepython = {env:PYTHON:python3}\n+passenv =\n+    GITHUB_OUTPUT\n+    CI\n+deps = -r{toxinidir}/misc/requirements/requirements-dev.txt\n+commands = {envpython} scripts/dev/update_version.py {posargs}\n+\n+[testenv:build-release{,-qt5}]\n basepython = {env:PYTHON:python3}\n passenv = *\n+# Override default PyQt6 from [testenv]\n+setenv =\n+    qt5: QUTE_QT_WRAPPER=PyQt5\n usedevelop = true\n deps =\n     -r{toxinidir}/requirements.txt\n     -r{toxinidir}/misc/requirements/requirements-tox.txt\n-    -r{toxinidir}/misc/requirements/requirements-pyqt.txt\n+    -r{toxinidir}/misc/requirements/requirements-docs.txt\n+    !qt5: -r{toxinidir}/misc/requirements/requirements-pyqt.txt\n+    qt5: -r{toxinidir}/misc/requirements/requirements-pyqt-5.txt\n     -r{toxinidir}/misc/requirements/requirements-dev.txt\n     -r{toxinidir}/misc/requirements/requirements-pyinstaller.txt\n commands =\n-    {envpython} {toxinidir}/scripts/dev/build_release.py {posargs}\n+    !qt5: {envpython} {toxinidir}/scripts/dev/build_release.py {posargs}\n+    qt5: {envpython} {toxinidir}/scripts/dev/build_release.py --qt5 {posargs}\n+\n+[testenv:package]\n+basepython = {env:PYTHON:python3}\n+setenv =\n+    PYTHONWARNINGS=error,default:pkg_resources is deprecated as an API.:DeprecationWarning\n+deps =\n+    -r{toxinidir}/requirements.txt\n+    -r{toxinidir}/misc/requirements/requirements-dev.txt\n+commands = {envpython} -m build\ndiff --git a/www/header.asciidoc b/www/header.asciidoc\nindex 66f6f2bb3..111f03451 100644\n--- a/www/header.asciidoc\n+++ b/www/header.asciidoc\n@@ -24,9 +24,11 @@\n part-time on qutebrowser, funded by donations.\n \nTo sustain this for a long\n time, your help is needed! See the\n-GitHub Sponsors page for more\n-information. Depending on your sign-up date and how long you keep a certain\n-level, you can get qutebrowser t-shirts, stickers and more!\n+GitHub Sponsors page or\n+alternative\n+donation methods for more information. Depending on your sign-up date and\n+how long you keep a certain level, you can get qutebrowser t-shirts, stickers\n+and more!\n \n \n +++\n", "creation_timestamp": "2026-06-30T02:27:28.045833Z"}