Merge remote-tracking branch 'upstream/main' into tree-tabs-integration

Main conflicts were with:
* 97104b2 Use builtin list/dict/set/... types for annotations
* 4d069b8 Use str.removeprefix() and str.removesuffix()
* https://github.com/qutebrowser/qutebrowser/pull/8345 use dosctrings
  for multiline strings in gherkin (still need to migrate the new tree
  tabs test file)

Conflicts:
       qutebrowser/browser/commands.py
       qutebrowser/mainwindow/mainwindow.py
       qutebrowser/mainwindow/tabbedbrowser.py
       tests/end2end/features/conftest.py
       tests/end2end/features/sessions.feature
This commit is contained in:
toofar 2025-03-16 12:37:01 +13:00
commit 079c58b593
315 changed files with 4306 additions and 1670 deletions

View File

@ -1,19 +0,0 @@
[bumpversion]
current_version = 3.1.0
commit = True
message = Release v{new_version}
tag = True
sign_tags = True
tag_name = v{new_version}
[bumpversion:file:qutebrowser/__init__.py]
parse = __version__ = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
[bumpversion:file:misc/org.qutebrowser.qutebrowser.appdata.xml]
search = <!-- Add new releases here -->
replace = <!-- Add new releases here -->
<release version="{new_version}" date="{now:%Y-%m-%d}"/>
[bumpversion:file:doc/changelog.asciidoc]
search = (unreleased)
replace = ({now:%Y-%m-%d})

26
.bumpversion.toml Normal file
View File

@ -0,0 +1,26 @@
[tool.bumpversion]
current_version = "3.4.0"
commit = true
message = "Release v{new_version}"
tag = true
sign_tags = true
tag_name = "v{new_version}"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"]
allow_dirty = false
[[tool.bumpversion.files]]
filename = "qutebrowser/__init__.py"
search = "__version__ = \"{current_version}\""
replace = "__version__ = \"{new_version}\""
[[tool.bumpversion.files]]
filename = "misc/org.qutebrowser.qutebrowser.appdata.xml"
search = "<!-- Add new releases here -->"
replace = """<!-- Add new releases here -->
<release version='{new_version}' date='{now:%Y-%m-%d}'/>"""
[[tool.bumpversion.files]]
filename = "doc/changelog.asciidoc"
search = "(unreleased)"
replace = "({now:%Y-%m-%d})"

View File

@ -58,7 +58,7 @@ ignore =
PT004,
PT011,
PT012
min-version = 3.8.0
min-version = 3.9.0
max-complexity = 12
per-file-ignores =
qutebrowser/api/hook.py : N801

View File

@ -10,7 +10,7 @@ on:
jobs:
tests:
if: "github.repository == 'qutebrowser/qutebrowser'"
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
timeout-minutes: 45
strategy:
fail-fast: false
@ -27,6 +27,7 @@ jobs:
PY_COLORS: "1"
DOCKER: "${{ matrix.image }}"
CI: true
TMPDIR: "${{ runner.temp }}"
volumes:
# Hardcoded because we can't use ${{ runner.temp }} here apparently.
- /home/runner/work/_temp/:/home/runner/work/_temp/
@ -37,12 +38,30 @@ jobs:
persist-credentials: false
- name: Set up problem matchers
run: "python scripts/dev/ci/problemmatchers.py py3 ${{ runner.temp }}"
- name: Upgrade 3rd party assets
run: "tox exec -e ${{ matrix.testenv }} -- python scripts/dev/update_3rdparty.py --gh-token ${{ secrets.GITHUB_TOKEN }} --modern-pdfjs"
if: "endsWith(matrix.image, '-qt6')"
- name: Run tox
run: dbus-run-session tox -e ${{ matrix.testenv }}
- name: Gather info
id: info
run: |
echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
shell: bash
if: failure()
- name: Upload screenshots
uses: actions/upload-artifact@v4
with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}"
path: |
${{ runner.temp }}/pytest-of-user/pytest-current/pytest-screenshots/*.png
if-no-files-found: ignore
if: failure()
irc:
timeout-minutes: 2
continue-on-error: true
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
needs: [tests]
if: "always() && github.repository == 'qutebrowser/qutebrowser'"
steps:

View File

@ -14,7 +14,7 @@ jobs:
linters:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
timeout-minutes: 10
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
@ -50,15 +50,15 @@ jobs:
python-version: '3.10'
- uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version: '22.x'
if: "matrix.testenv == 'eslint'"
- name: Set up problem matchers
run: "python scripts/dev/ci/problemmatchers.py ${{ matrix.testenv }} ${{ runner.temp }}"
- name: Install dependencies
run: |
[[ ${{ matrix.testenv }} == eslint ]] && npm install -g 'eslint@<9.0.0'
[[ ${{ matrix.testenv }} == docs ]] && sudo apt-get update && sudo apt-get install --no-install-recommends asciidoc libegl1-mesa
[[ ${{ matrix.testenv }} == vulture || ${{ matrix.testenv }} == pylint ]] && sudo apt-get update && sudo apt-get install --no-install-recommends libegl1-mesa
[[ ${{ matrix.testenv }} == docs ]] && sudo apt-get update && sudo apt-get install --no-install-recommends asciidoc libegl1
[[ ${{ matrix.testenv }} == vulture || ${{ matrix.testenv }} == pylint ]] && sudo apt-get update && sudo apt-get install --no-install-recommends libegl1
if [[ ${{ matrix.testenv }} == shellcheck ]]; then
scversion="stable"
bindir="$HOME/.local/bin"
@ -86,7 +86,7 @@ jobs:
tests-docker:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
timeout-minutes: 45
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04 # not 24.04 because sandboxing fails by default (#8424)
strategy:
fail-fast: false
matrix:
@ -107,6 +107,7 @@ jobs:
DOCKER: "${{ matrix.image }}"
CI: true
PYTEST_ADDOPTS: "--color=yes"
TMPDIR: "${{ runner.temp }}"
volumes:
# Hardcoded because we can't use ${{ runner.temp }} here apparently.
- /home/runner/work/_temp/:/home/runner/work/_temp/
@ -119,6 +120,21 @@ jobs:
run: "python scripts/dev/ci/problemmatchers.py tests ${{ runner.temp }}"
- name: Run tox
run: "dbus-run-session -- tox -e ${{ matrix.testenv }}"
- name: Gather info
id: info
run: |
echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
shell: bash
if: failure()
- name: Upload screenshots
uses: actions/upload-artifact@v4
with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}"
path: |
${{ runner.temp }}/pytest-of-user/pytest-current/pytest-screenshots/*.png
if-no-files-found: ignore
if: failure()
tests:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
@ -128,10 +144,10 @@ jobs:
fail-fast: false
matrix:
include:
### PyQt 5.15.2 (Python 3.8)
- testenv: py37-pyqt5152
os: ubuntu-20.04
python: "3.8"
### PyQt 5.15.2 (Python 3.9)
- testenv: py39-pyqt5152
os: ubuntu-22.04
python: "3.9"
### PyQt 5.15 (Python 3.10, with coverage)
# FIXME:qt6
# - testenv: py310-pyqt515-cov
@ -139,19 +155,19 @@ jobs:
# python: "3.10"
### PyQt 5.15 (Python 3.11)
- testenv: py311-pyqt515
os: ubuntu-20.04
os: ubuntu-22.04
python: "3.11"
### PyQt 6.2 (Python 3.8)
- testenv: py37-pyqt62
os: ubuntu-20.04
python: "3.8"
### PyQt 6.3 (Python 3.8)
- testenv: py38-pyqt63
os: ubuntu-20.04
python: "3.8"
### PyQt 6.2 (Python 3.9)
- testenv: py39-pyqt62
os: ubuntu-22.04
python: "3.9"
### PyQt 6.3 (Python 3.9)
- testenv: py39-pyqt63
os: ubuntu-22.04
python: "3.9"
## PyQt 6.4 (Python 3.9)
- testenv: py39-pyqt64
os: ubuntu-20.04
os: ubuntu-22.04
python: "3.9"
### PyQt 6.5 (Python 3.10)
- testenv: py310-pyqt65
@ -165,20 +181,32 @@ jobs:
- testenv: py312-pyqt66
os: ubuntu-22.04
python: "3.12"
### macOS Big Sur
- testenv: py312-pyqt66
os: macos-11
### PyQt 6.7 (Python 3.11)
- testenv: py311-pyqt67
os: ubuntu-22.04
python: "3.11"
### PyQt 6.7 (Python 3.12)
- testenv: py312-pyqt67
os: ubuntu-22.04
python: "3.12"
### PyQt 6.8 (Python 3.13)
- testenv: py313-pyqt68
os: ubuntu-24.04
python: "3.13"
### macOS Ventura
- testenv: py313-pyqt68
os: macos-13
python: "3.13"
args: "tests/unit" # Only run unit tests on macOS
### macOS Monterey
- testenv: py312-pyqt66
os: macos-12
python: "3.12"
### macOS Sonoma (M1 runner)
- testenv: py313-pyqt68
os: macos-14
python: "3.13"
args: "tests/unit" # Only run unit tests on macOS
### Windows
- testenv: py312-pyqt66
- testenv: py313-pyqt68
os: windows-2019
python: "3.12"
python: "3.13"
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@v4
@ -200,12 +228,17 @@ jobs:
- name: Install apt dependencies
run: |
sudo apt-get update
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
sudo apt-get install --no-install-recommends libyaml-dev libegl1 libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0
if: "startsWith(matrix.os, 'ubuntu-')"
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U -r misc/requirements/requirements-tox.txt
- name: Upgrade 3rd party assets
run: "tox exec -e ${{ matrix.testenv }} -- python scripts/dev/update_3rdparty.py --gh-token ${{ secrets.GITHUB_TOKEN }}"
if: "startsWith(matrix.os, 'windows-')"
- name: "Set TMPDIR for pytest"
run: 'echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV"'
- name: "Run ${{ matrix.testenv }}"
run: "dbus-run-session -- tox -e ${{ matrix.testenv }} -- ${{ matrix.args }}"
if: "startsWith(matrix.os, 'ubuntu-')"
@ -217,16 +250,31 @@ jobs:
if: "failure()"
- name: Upload coverage
if: "endsWith(matrix.testenv, '-cov')"
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
with:
name: "${{ matrix.testenv }}"
- name: Gather info
id: info
run: |
echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
shell: bash
if: failure()
- name: Upload screenshots
uses: actions/upload-artifact@v4
with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.testenv }}-${{ matrix.os }}"
path: |
${{ runner.temp }}/pytest-of-runner/pytest-current/pytest-screenshots/*.png
if-no-files-found: ignore
if: failure()
codeql:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
permissions:
security-events: write
timeout-minutes: 15
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
@ -243,7 +291,7 @@ jobs:
irc:
timeout-minutes: 2
continue-on-error: true
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
needs: [linters, tests, tests-docker, codeql]
if: "always() && github.repository_owner == 'qutebrowser'"
steps:

View File

@ -8,7 +8,7 @@ on:
jobs:
docker:
if: "github.repository == 'qutebrowser/qutebrowser'"
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
@ -32,7 +32,7 @@ jobs:
with:
username: qutebrowser
password: ${{ secrets.DOCKER_TOKEN }}
- uses: docker/build-push-action@v5
- uses: docker/build-push-action@v6
with:
file: scripts/dev/ci/docker/Dockerfile
context: .
@ -42,7 +42,7 @@ jobs:
irc:
timeout-minutes: 2
continue-on-error: true
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
needs: [docker]
if: "always() && github.repository == 'qutebrowser/qutebrowser'"
steps:

View File

@ -14,30 +14,22 @@ jobs:
fail-fast: false
matrix:
include:
- os: macos-11
toxenv: build-release-qt5
name: qt5-macos
- os: windows-2019
toxenv: build-release-qt5
name: qt5-windows
- os: macos-11
args: --debug
toxenv: build-release-qt5
name: qt5-macos-debug
- os: windows-2019
args: --debug
toxenv: build-release-qt5
name: qt5-windows-debug
- os: macos-11
- os: macos-13
toxenv: build-release
name: macos
name: macos-intel
- os: macos-14
toxenv: build-release
name: macos-apple-silicon
- os: windows-2019
toxenv: build-release
name: windows
- os: macos-11
- os: macos-13
args: --debug
toxenv: build-release
name: macos-debug
name: macos-debug-intel
- os: macos-14
toxenv: build-release
name: macos-debug-apple-silicon
- os: windows-2019
args: --debug
toxenv: build-release
@ -51,7 +43,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.13"
- name: Install dependencies
run: |
python -m pip install -U pip
@ -83,7 +75,7 @@ jobs:
irc:
timeout-minutes: 2
continue-on-error: true
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
needs: [pyinstaller]
if: "always() && github.repository == 'qutebrowser/qutebrowser'"
steps:

View File

@ -21,17 +21,17 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: '3.8'
python-version: '3.9'
- name: Recompile requirements
run: "python3 scripts/dev/recompile_requirements.py ${{ github.event.input.environments }}"
id: requirements
- name: Install apt dependencies
run: |
sudo apt-get update
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
sudo apt-get install --no-install-recommends libyaml-dev libegl1 libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0 asciidoc python3-venv xvfb
- name: Install dependencies
run: |
python -m pip install -U pip
@ -41,7 +41,7 @@ jobs:
- name: Run qutebrowser smoke test
run: "xvfb-run .venv/bin/python3 -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ':later 500 quit'"
- name: Create pull request
uses: peter-evans/create-pull-request@v6
uses: peter-evans/create-pull-request@v7
with:
committer: qutebrowser bot <bot@qutebrowser.org>
author: qutebrowser bot <bot@qutebrowser.org>

View File

@ -16,17 +16,17 @@ on:
python_version:
description: 'Python version'
required: true
default: '3.12'
default: '3.13'
type: choice
options:
- '3.8'
- '3.9'
- '3.10'
- '3.11'
- '3.12'
- '3.13'
jobs:
prepare:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
version: ${{ steps.bump.outputs.version }}
@ -126,9 +126,10 @@ jobs:
strategy:
matrix:
include:
- os: macos-11
- os: macos-13
- os: macos-14
- os: windows-2019
- os: ubuntu-20.04
- os: ubuntu-24.04
runs-on: "${{ matrix.os }}"
timeout-minutes: 45
needs: [prepare]
@ -158,7 +159,7 @@ jobs:
if: ${{ startsWith(matrix.os, 'ubuntu-') }}
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends libegl1-mesa libxml2-utils docbook-xml xsltproc docbook-xsl
sudo apt-get install --no-install-recommends libegl1 libxml2-utils docbook-xml xsltproc docbook-xsl
- name: Install dependencies
run: |
python -m pip install -U pip
@ -172,7 +173,7 @@ jobs:
TWINE_PASSWORD: ${{ secrets.QUTEBROWSER_BOT_PYPI_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
finalize:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
timeout-minutes: 5
needs: [prepare, release]
permissions:
@ -192,7 +193,7 @@ jobs:
irc:
timeout-minutes: 2
continue-on-error: true
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
needs: [prepare, release, finalize]
if: "${{ always() }}"
steps:

View File

@ -1,5 +1,5 @@
[mypy]
python_version = 3.8
python_version = 3.9
### --strict
warn_unused_configs = True
@ -20,6 +20,7 @@ strict_equality = True
warn_unreachable = True
disallow_any_unimported = True
enable_error_code = ignore-without-code
strict_bytes = True
### Output
show_error_context = True

View File

@ -16,7 +16,7 @@ load-plugins=qute_pylint.config,
pylint.extensions.dunder
persistent=n
py-version=3.8
py-version=3.9
[MESSAGES CONTROL]
enable=all
@ -71,7 +71,8 @@ argument-rgx=[a-z_][a-z0-9_]{0,30}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$
docstring-min-length=3
no-docstring-rgx=(^_|^main$)
class-const-naming-style = snake_case
class-const-naming-style=snake_case
max-positional-arguments=7
[FORMAT]
# FIXME:v4 (lint) down to 88 again once we use black

View File

@ -44,7 +44,7 @@ image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
Downloads
---------
See the https://github.com/qutebrowser/qutebrowser/releases[github releases
See the https://github.com/qutebrowser/qutebrowser/releases[GitHub releases
page] for available downloads and the link:doc/install.asciidoc[INSTALL] file for
detailed instructions on how to get qutebrowser running on various platforms.
@ -84,7 +84,7 @@ Requirements
The following software and libraries are required to run qutebrowser:
* https://www.python.org/[Python] 3.8 or newer
* https://www.python.org/[Python] 3.9 or newer
* https://www.qt.io/[Qt], either 6.2.0 or newer, or 5.15.0 or newer, with the following modules:
- QtCore / qtbase
- QtQuick (part of qtbase or qtdeclarative in some distributions)
@ -105,10 +105,6 @@ websites and using it for transmission of sensitive data._
* https://palletsprojects.com/p/jinja/[jinja2]
* https://github.com/yaml/pyyaml[PyYAML]
On Python 3.8, the following backport is also required:
* https://importlib-resources.readthedocs.io/[importlib_resources]
On macOS, the following libraries are also required:
* https://pyobjc.readthedocs.io/en/latest/[pyobjc-core and pyobjc-framework-Cocoa]
@ -252,9 +248,9 @@ main inspiration for qutebrowser)
https://github.com/akhodakivskiy/VimFx[VimFx] (seems to offer a
https://gir.st/blog/legacyfox.htm[hack] to run on modern Firefox releases),
https://github.com/shinglyu/QuantumVim[QuantumVim],
https://github.com/ueokande/vim-vixen[Vim Vixen] (ESR only),
https://github.com/ueokande/vim-vixen[Vim Vixen],
https://github.com/amedama41/vvimpulation[VVimpulation],
https://krabby.netlify.com/[Krabby]
https://krabby.netlify.app/[Krabby]
* Chrome/Chromium addons:
https://github.com/k2nr/ViChrome/[ViChrome],
https://github.com/jinzhu/vrome[Vrome],

View File

@ -15,23 +15,181 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
[[v3.2.0]]
v3.2.0 (unreleased)
[[v3.4.1]]
v3.4.1 (unreleased)
-------------------
Changed
~~~~~~~
- The `content.headers.user_agent` setting now has a new
`{upstream_browser_version_short}` template field, which is the
upstream/Chromium version but shortened to only major version.
Fixed
~~~~~
- Crash when trying to use the `DocumentPictureInPicture` JS API, such as done
by the new Google Workspaces Huddle feature. The API is unsupported by
QtWebEngine and now correctly disabled on the JS side. (#8449)
- Crash when a buggy notification presenter returns a duplicate ID (now an
error is shown instead).
- The default user agent now only contains the shortened Chromium version
number, which fixes overzealous blocking on ScienceDirect.
[[v3.4.0]]
v3.4.0 (2024-12-14)
-------------------
Removed
~~~~~~~
- Support for Python 3.8 is dropped, and Python 3.9 is now required. (#8325)
- Support for macOS 12 Monterey is now dropped, and binaries will be built on
macOS 13 Ventura. (#8327)
- When using the installer on Windows 10, build 1809 or newer is now required
(previous versions required 1607 or newer, but that's not officialy supported by
Qt upstream). (#8336)
Changed
~~~~~~~
- Windows/macOS binaries are now built with Qt 6.8.1. (#8242)
- Based on Chromium 122.0.6261.171
- With security patches up to 131.0.6778.70
- Windows/macOS binaries are now using Python 3.13. (#8205)
- The `.desktop` file now also declares qutebrowser as a valid viewer for
`image/webp`. (#8340)
- Updated mimetype information for getting a suitable extension when downloading
a `data:` URL.
- The `content.javascript.clipboard` setting now defaults to "ask", which on
Qt 6.8+ will prompt the user to grant clipboard access. On older Qt versions,
this is still equivalent to `"none"` and needs to be set manually. (#8348)
- If a XHR request made via JS sets a custom `Accept-Language` header, it now
correctly has precedence over the global `content.headers.accept_language`
setting (but not per-domain overrides). This fixes subtle JS issues on
websites that rely on the custom header being sent for those requests, and
e.g. block the requests server-side otherwise. (#8370)
- Our packaging scripts now prefer the "legacy"/"for older browsers" PDF.js
build as their normal release only supports the latest Chromium version and
might break in qutebrowser on updates. **Note to packagers:** If there's a
PDF.js package in your distribution as an (optional) qutebrowser dependency,
consider also switching to this variant (same code, built differently).
Fixed
~~~~~
- Crash with recent Jinja/Markupsafe versions when viewing a finished userscript
(or potentially editor) process via `:process`.
- `scripts/open_url_in_instance.sh` now avoids `echo -n`, thus running
correctly on POSIX sh. (#8409)
- Added a workaround for a bogus QtWebEngine warning about missing spell
checking dictionaries. (#8330)
[[v3.3.1]]
v3.3.1 (2024-10-12)
-------------------
Fixed
~~~~~
- Updated the workaround for Google sign-in issues.
[[v3.3.0]]
v3.3.0 (2024-10-12)
-------------------
Added
~~~~~
- Added the `qt.workarounds.disable_hangouts_extension` setting,
for disabling the Google Hangouts extension built into Chromium/QtWebEngine.
- Failed end2end tests will now save screenshots of the browser window when
run under xvfb (the default on linux). Screenshots will be under
`$TEMP/pytest-current/pytest-screenshots/` or attached to the GitHub actions
run as an artifact. (#7625)
Removed
~~~~~~~
- Support for macOS 11 Big Sur is dropped. Binaries are now built on macOS 12
Monterey and are unlikely to still run on older macOS versions.
Changed
~~~~~~~
- The qute-pass userscript now has better support for internationalized domain
names when using the pass backend - both domain names and secret paths are
normalized before comparing (#8133)
- Ignored URL query parameters (via `url.yank_ignored_parameters`) are now
respected when yanking any URL (for example, through hints with `hint links
yank`). The `{url:yank}` substitution has also been added as a version of
`{url}` that respects ignored URL query parameters. (#7879)
- Windows and macOS releases now bundle Qt 6.7.3, which includes security fixes
up to Chromium 129.0.6668.58.
Fixed
~~~~~
- A minor memory leak of QItemSelectionModels triggered by closing the
completion dialog has been resolved. (#7950)
- The link to the chrome https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns/[URL match pattern]
documentation in our settings docs now loads a live page again. (#8268)
- A rare crash when on Qt 6, a renderer process terminates with an unknown
termination reason.
- Updated the workaround for Google sign-in issues.
[[v3.2.1]]
v3.2.1 (2024-06-25)
-------------------
Added
~~~~~
- There is now a separate macOS release built for Apple Silicon. A Universal
Binary might follow with a later release.
Changed
~~~~~~~
- Windows and macOS releases now bundle Qt 6.7.2, which includes security fixes
up to Chromium 125.0.6422.142.
Fixed
~~~~~
- When the selected Qt wrapper is unavailable, qutebrowser now again shows a
GUI error message instead of only an exception in the terminal.
[[v3.2.0]]
v3.2.0 (2024-06-03)
-------------------
Deprecated
~~~~~~~~~~
- This will be the last feature release supporting macOS 11 Big Sur.
Starting with qutebrowser v3.3.0, macOS 12 Monterey will be the oldest
supported version.
Added
~~~~~
- When qutebrowser receives a SIGHUP it will now reload any config.py file
in use (same as the `:config-source` command does). (#8108)
- The Chromium security patch version is now shown in the backend string in
--version and :version. This reflects the latest Chromium version that
`--version` and `:version`. This reflects the latest Chromium version that
security fixes have been backported to the base QtWebEngine version from.
(#7187)
Changed
~~~~~~~
- Windows and macOS releases now ship with Qt 6.7.1, which is based on Chromium
118.0.5993.220 with security patches up to 124.0.6367.202.
- With QtWebEngine 6.7+, the `colors.webpage.darkmode.enabled` setting can now
be changed at runtime and supports URL patterns (#8182).
- A few more completions will now match search terms in any order:
`:quickmark-*`, `:bookmark-*`, `:tab-take` and `:tab-select` (for the quick
and bookmark categories). (#7955)
@ -44,14 +202,19 @@ Fixed
~~~~~
- `input.insert_mode.auto_load` sometimes not triggering due to a race
condition.
condition. (#8145)
- Worked around qutebrowser quitting when closing a KDE file dialog due to a Qt
bug.
[[v3.1.1]]
v3.1.1 (unreleased)
-------------------
bug. (#8143)
- Trying to use qutebrowser after it's been deleted/moved on disk (e.g. after a
Python upgrade) should now not crash anymore.
- When the QtWebEngine resources dir couldn't be found, qutebrowser now doesn't
crash anymore (but QtWebEngine still might).
- Fixed a rare crash in the completion widget when there was no selection model
when we went to clear that, probably when leaving a mode. (#7901)
- Worked around a minor issue around QTimers on Windows where the IPC server
could close the socket early. (#8191)
- The latest PDF.js release (v4.2.67) is now supported when backed by
QtWebEngine 6.6+ (#8170)
[[v3.1.0]]
v3.1.0 (2023-12-08)
@ -3669,7 +3832,7 @@ Fixed
- Continuing a search after clearing it now works correctly.
- The tabbar and completion should now be more consistently and correctly
styled with various system styles.
- Applying styiles in `qt5ct` now shouldn't crash anymore.
- Applying styles in `qt5ct` now shouldn't crash anymore.
- The validation for colors in stylesheets is now less strict,
allowing for all valid Qt values.
- `data:` URLs now aren't added to the history anymore.

View File

@ -41,7 +41,7 @@ If you want to find something useful to do, check the
https://github.com/qutebrowser/qutebrowser/issues[issue tracker]. Some
pointers:
* https://github.com/qutebrowser/qutebrowser/labels/easy[Issues which should
* https://github.com/qutebrowser/qutebrowser/contribute[Issues which should
be easy to solve]
* https://github.com/qutebrowser/qutebrowser/labels/component%3A%20docs[Documentation issues which require little/no coding]
@ -111,9 +111,9 @@ unittests and several linters/checkers.
Currently, the following tox environments are available:
* Tests using https://www.pytest.org[pytest]:
- `py38`, `py39`, ...: Run pytest for python 3.8/3.9/... with the system-wide PyQt.
- `py38-pyqt515`, ..., `py38-pyqt65`: Run pytest with the given PyQt version (`py39-*` etc. also works).
- `py38-pyqt515-cov`: Run with coverage support (other Python/PyQt versions work too).
- `py39`, `py310`, ...: Run pytest for python 3.9/3.10/... with the system-wide PyQt.
- `py39-pyqt515`, ..., `py39-pyqt65`: Run pytest with the given PyQt version (`py310-*` etc. also works).
- `py39-pyqt515-cov`: Run with coverage support (other Python/PyQt versions work too).
* `flake8`: Run various linting checks via https://pypi.python.org/pypi/flake8[flake8].
* `vulture`: Run https://pypi.python.org/pypi/vulture[vulture] to find
unused code portions.
@ -171,16 +171,16 @@ Examples:
----
# run only pytest tests which failed in last run:
tox -e py38 -- --lf
tox -e py39 -- --lf
# run only the end2end feature tests:
tox -e py38 -- tests/end2end/features
tox -e py39 -- tests/end2end/features
# run everything with undo in the generated name, based on the scenario text
tox -e py38 -- tests/end2end/features/test_tabs_bdd.py -k undo
tox -e py39 -- tests/end2end/features/test_tabs_bdd.py -k undo
# run coverage test for specific file (updates htmlcov/index.html)
tox -e py38-cov -- tests/unit/browser/test_webelem.py
tox -e py39-cov -- tests/unit/browser/test_webelem.py
----
Specifying the backend for tests
@ -192,6 +192,28 @@ specific one you can set either of a) the environment variable QUTE_TESTS_BACKEN
, or b) the command line argument --qute-backend, to the desired backend
(webkit/webengine).
If you need an environment with webkit installed to do testing while we still
support it (see #4039) you can re-use the docker container used for the CI
test runs which has PyQt5Webkit installed from the archlinux package archives.
Examples:
----
# Get a bash shell in the docker container with
# a) the current directory mounted at /work in the container
# b) the container using the X11 display :27 (for example, a Xephyr instance) from the host
# c) the tox and hypothesis dirs set to somewhere in the container that it can write to
# d) the system site packages available in the tox venv so you can use PyQt
# from the OS without having to run the link_pyqt script
docker run -it -v $PWD:/work:ro -w /work -e QUTE_TESTS_BACKEND=webkit -e DISPLAY=:27 -v /tmp/.X11-unix:/tmp/.X11-unix -e TOX_WORK_DIR="/home/user/.tox" -e HYPOTHESIS_EXAMPLES_DIR="/home/user/.hypothesis/examples" -e VIRTUALENV_SYSTEM_SITE_PACKAGES=True qutebrowser/ci:archlinux-webkit bash
# Start a qutebrowser temporary basedir in the appropriate tox environment to
# play with
tox exec -e py-qt5 -- python3 -m qutebrowser -T --backend webkit
# Run tests, passing positional args through to pytest.
tox -e py-qt5 -- tests/unit
----
Profiling
~~~~~~~~~
@ -767,10 +789,12 @@ New PyQt release
qutebrowser release
~~~~~~~~~~~~~~~~~~~
* Make sure there are no unstaged changes and the tests are green.
* Make sure there are no unstaged or unpushed changes.
* Make sure CI is reasonably green.
* Make sure all issues with the related milestone are closed.
* Mark the https://github.com/qutebrowser/qutebrowser/milestones[milestone] as closed.
* Consider updating the completions for `content.headers.user_agent` in `configdata.yml`.
* Consider updating the completions for `content.headers.user_agent` in `configdata.yml`
and the Firefox UA in `qutebrowser/browser/webengine/webenginesettings.py`.
* Minor release: Consider updating some files from main:
- `misc/requirements/` and `requirements.txt`
- `scripts/`

View File

@ -430,7 +430,7 @@ allowing him to work part-time on qutebrowser. If you keep your donation level
for long enough, you can get some qutebrowser stickers!
Why GitHub Sponsors?::
GitHub Sponsors is a crowdfundign platform nicely integrated with
GitHub Sponsors is a crowdfunding platform nicely integrated with
qutebrowser's existing GitHub page and a better offering than alternatives such
as Patreon or Liberapay.
+

View File

@ -17,6 +17,8 @@ For command arguments, there are also some variables you can use:
- `{url:host}`, `{url:domain}`, `{url:auth}`, `{url:scheme}`, `{url:username}`,
`{url:password}`, `{url:port}`, `{url:path}` and `{url:query}`
expand to the respective parts of the current URL
- `{url:yank}` expands to the URL of the current page but strips all the query
parameters in the `url.yank_ignored_parameters` setting.
- `{title}` expands to the current page's title
- `{clipboard}` expands to the clipboard contents
- `{primary}` expands to the primary selection contents

View File

@ -31,7 +31,7 @@ patterns. The link:settings{outfilesuffix}[settings documentation] marks such
settings with "This setting supports URL patterns.
The syntax is based on Chromium's
https://developer.chrome.com/docs/extensions/mv3/match_patterns/[URL pattern syntax].
https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns/[URL pattern syntax].
As an extension, the scheme and path can be left off as a short-hand syntax, so
`example.com` is equivalent to `*://example.com/*`.
@ -416,6 +416,7 @@ Pre-built colorschemes
- https://github.com/gicrisf/qute-city-lights[City Lights (matte dark)]
- https://github.com/catppuccin/qutebrowser[Catppuccin]
- https://github.com/iruzo/matrix-qutebrowser[Matrix]
- https://github.com/harmtemolder/qutebrowser-solarized[Solarized]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
@ -452,7 +453,7 @@ Various emacs/conkeror-like keybinding configs exist:
- https://gitlab.com/Kaligule/qutebrowser-emacs-config/blob/master/config.py[Kaligule]
- https://web.archive.org/web/20210512185023/https://me0w.net/pit/1540882719[nm0i]
- https://www.reddit.com/r/qutebrowser/comments/eh10i7/config_share_qute_with_emacs_keybindings/[jasonsun0310]
- https://git.sr.ht/~willvaughn/dots/tree/mjolnir/item/.config/qutebrowser/qutemacs.py[willvaughn]
- https://git.sr.ht/~willvaughn/dots/tree/main/item/.config/qutebrowser/qutemacs.py[willvaughn]
It's also mostly possible to get rid of modal keybindings by setting
`input.insert_mode.auto_enter` to `false`, and `input.forward_unbound_keys` to

View File

@ -302,6 +302,7 @@
|<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine.
|<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling.
|<<qt.workarounds.disable_accelerated_2d_canvas,qt.workarounds.disable_accelerated_2d_canvas>>|Disable accelerated 2d canvas to avoid graphical glitches.
|<<qt.workarounds.disable_hangouts_extension,qt.workarounds.disable_hangouts_extension>>|Disable the Hangouts extension.
|<<qt.workarounds.locale,qt.workarounds.locale>>|Work around locale parsing issues in QtWebEngine 5.15.3.
|<<qt.workarounds.remove_service_workers,qt.workarounds.remove_service_workers>>|Delete the QtWebEngine Service Worker directory on every start.
|<<scrolling.bar,scrolling.bar>>|When/how to show the scrollbar.
@ -361,7 +362,7 @@
|<<url.open_base_url,url.open_base_url>>|Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
|<<url.searchengines,url.searchengines>>|Search engines which can be used via the address bar.
|<<url.start_pages,url.start_pages>>|Page(s) to open at the start.
|<<url.yank_ignored_parameters,url.yank_ignored_parameters>>|URL parameters to strip with `:yank url`.
|<<url.yank_ignored_parameters,url.yank_ignored_parameters>>|URL parameters to strip when yanking a URL.
|<<window.hide_decoration,window.hide_decoration>>|Hide the window decoration.
|<<window.title_format,window.title_format>>|Format to use for the window title. The same placeholders like for
|<<window.transparent,window.transparent>>|Set the main window background to transparent.
@ -745,12 +746,12 @@ Default:
* +pass:[xO]+: +pass:[cmd-set-text :open -b -r {url:pretty}]+
* +pass:[xo]+: +pass:[cmd-set-text -s :open -b]+
* +pass:[yD]+: +pass:[yank domain -s]+
* +pass:[yM]+: +pass:[yank inline [{title}\]({url}) -s]+
* +pass:[yM]+: +pass:[yank inline [{title}\]({url:yank}) -s]+
* +pass:[yP]+: +pass:[yank pretty-url -s]+
* +pass:[yT]+: +pass:[yank title -s]+
* +pass:[yY]+: +pass:[yank -s]+
* +pass:[yd]+: +pass:[yank domain]+
* +pass:[ym]+: +pass:[yank inline [{title}\]({url})]+
* +pass:[ym]+: +pass:[yank inline [{title}\]({url:yank})]+
* +pass:[yp]+: +pass:[yank pretty-url]+
* +pass:[yt]+: +pass:[yank title]+
* +pass:[yy]+: +pass:[yank]+
@ -1695,6 +1696,7 @@ Default: +pass:[0.0]+
[[colors.webpage.darkmode.enabled]]
=== colors.webpage.darkmode.enabled
Render all web contents using a dark theme.
On QtWebEngine < 6.7, this setting requires a restart and does not support URL patterns, only the global setting is applied.
Example configurations from Chromium's `chrome://flags`:
- "With simple HSL/CIELAB/RGB-based inversion": Set
`colors.webpage.darkmode.algorithm` accordingly, and
@ -1702,7 +1704,7 @@ Example configurations from Chromium's `chrome://flags`:
- "With selective image inversion": qutebrowser default settings.
This setting requires a restart.
This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebEngine backend.
@ -2290,6 +2292,8 @@ The following placeholders are defined:
* `{upstream_browser_key}`: "Version" for QtWebKit, "Chrome" for
QtWebEngine.
* `{upstream_browser_version}`: The corresponding Safari/Chrome version.
* `{upstream_browser_version_short}`: The corresponding Safari/Chrome
version, but only with its major version.
* `{qutebrowser_version}`: The currently running qutebrowser version.
The default value is equal to the unchanged user agent of
@ -2304,7 +2308,7 @@ This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,FormatString>>
Default: +pass:[Mozilla/5.0 ({os_info}) AppleWebKit/{webkit_version} (KHTML, like Gecko) {qt_key}/{qt_version} {upstream_browser_key}/{upstream_browser_version} Safari/{webkit_version}]+
Default: +pass:[Mozilla/5.0 ({os_info}) AppleWebKit/{webkit_version} (KHTML, like Gecko) {qt_key}/{qt_version} {upstream_browser_key}/{upstream_browser_version_short} Safari/{webkit_version}]+
[[content.hyperlink_auditing]]
=== content.hyperlink_auditing
@ -2360,18 +2364,20 @@ Default: +pass:[false]+
=== content.javascript.clipboard
Allow JavaScript to read from or write to the clipboard.
With QtWebEngine, writing the clipboard as response to a user interaction is always allowed.
On Qt < 6.8, the `ask` setting is equivalent to `none` and permission needs to be granted manually via this setting.
This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,String>>
Type: <<types,JSClipboardPermission>>
Valid values:
* +none+: Disable access to clipboard.
* +access+: Allow reading from and writing to the clipboard.
* +access-paste+: Allow accessing the clipboard and pasting clipboard content.
* +ask+: Prompt when requested (grants 'access-paste' permission).
Default: +pass:[none]+
Default: +pass:[ask]+
[[content.javascript.enabled]]
=== content.javascript.enabled
@ -3906,7 +3912,7 @@ Chromium has various sandboxing layers, which should be enabled for normal brows
Open `chrome://sandbox` to see the current sandbox status.
Changing this setting is only recommended if you know what you're doing, as it **disables one of Chromium's security layers**. To avoid sandboxing being accidentally disabled persistently, this setting can only be set via `config.py`, not via `:set`.
See the Chromium documentation for more details:
- https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/linux/sandboxing.md[Linux] - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox.md[Windows] - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox_faq.md[FAQ (Windows-centric)]
- https://chromium.googlesource.com/chromium/src/\+/HEAD/sandbox/linux/README.md[Linux] - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox.md[Windows] - https://chromium.googlesource.com/chromium/src/\+/HEAD/sandbox/mac/README.md[Mac] - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox_faq.md[FAQ (Windows-centric)]
This setting requires a restart.
@ -4009,6 +4015,21 @@ Valid values:
Default: +pass:[auto]+
[[qt.workarounds.disable_hangouts_extension]]
=== qt.workarounds.disable_hangouts_extension
Disable the Hangouts extension.
The Hangouts extension provides additional APIs for Google domains only.
Hangouts has been replaced with Meet, which appears to work without this extension.
Note this setting gets ignored and the Hangouts extension is always disabled to avoid crashes on Qt 6.5.0 to 6.5.3 if dark mode is enabled, as well as on Qt 6.6.0.
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,Bool>>
Default: +pass:[false]+
[[qt.workarounds.locale]]
=== qt.workarounds.locale
Work around locale parsing issues in QtWebEngine 5.15.3.
@ -4792,7 +4813,7 @@ Default: +pass:[https://start.duckduckgo.com]+
[[url.yank_ignored_parameters]]
=== url.yank_ignored_parameters
URL parameters to strip with `:yank url`.
URL parameters to strip when yanking a URL.
Type: <<types,List of String>>
@ -4927,6 +4948,7 @@ Lists with duplicate flags are invalid. Each item is checked against the valid v
|FuzzyUrl|A URL which gets interpreted as search if needed.
|IgnoreCase|Whether to search case insensitively.
|Int|Base class for an integer setting.
|JSClipboardPermission|Permission for page JS to access the system clipboard.
|Key|A name of a key.
|List|A list of values.
@ -4966,6 +4988,6 @@ See the setting's valid values for more information on allowed values.
|Url|A URL as a string.
|UrlPattern|A match pattern for a URL.
See https://developer.chrome.com/apps/match_patterns for the allowed syntax.
See https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns for the allowed syntax.
|VerticalPosition|The position of the download bar.
|==============

View File

@ -45,7 +45,7 @@ by Debian's security support.
It's recommended to <<tox,install qutebrowser in a virtualenv>> with a newer PyQt/Qt binary instead.
If you need proprietary codec support or use an architecture not supported by Qt
binaries, starting with Ubuntu 22.04 and Debian Bookworm, it's possible to
install Qt 6 via apt. By using `mkvenv.py` with `--pyqt-type link` you get a
install Qt 6 via apt. By using `scripts/mkvenv.py` with `--pyqt-type link` you get a
newer qutebrowser running with:
- Ubuntu 22.04, Linux Mint 21: QtWebEngine 6.2.4 (based on Chromium 90 from mid-2021)
@ -54,7 +54,7 @@ newer qutebrowser running with:
Note you'll need some basic libraries to use the virtualenv-installed PyQt:
----
# 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
# apt install --no-install-recommends git ca-certificates python3 python3-venv libgl1 libxkbcommon-x11-0 libegl1 libfontconfig1 libglib2.0-0 libdbus-1-3 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-shape0 libnss3 libxcomposite1 libxdamage1 libxrender1 libxrandr2 libxtst6 libxi6 libasound2
----
Additional hints
@ -64,9 +64,9 @@ Additional hints
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]
for its binary builds. Thus, you will either need to live with
`:adblock-update` and `:download` being broken, or use `--pyqt-version 6.4` for
the `mkvenv.py` script to get an older Qt.
the `scripts/mkvenv.py` script to get an older Qt.
- If running from git, run the following to generate the documentation for the
`:help` command (the `mkvenv.py` script used with a virtualenv install already does
`:help` command (the `scripts/mkvenv.py` script used with a virtualenv install already does
this for you):
+
----
@ -103,12 +103,18 @@ To be able to play videos with proprietary codecs with QtWebEngine, you will
need to install an additional package from the RPM Fusion Free repository.
For more information see https://rpmfusion.org/Configuration.
With Qt 6 (recommended):
-----
# dnf install libavcodec-freeworld
-----
With Qt 5:
-----
# dnf install qt5-qtwebengine-freeworld
-----
It's currently unknown what the Qt 6 equivalent of this is.
On Archlinux
------------
@ -162,6 +168,13 @@ need to turn off the `bindist` flag for `dev-qt/qtwebengine`.
See the https://wiki.gentoo.org/wiki/Qutebrowser#USE_flags[Gentoo Wiki] for
more information.
To be able to use Kerberos authentication, you will need to turn on the
`kerberos` USE-flag system-wide and re-emerge `dev-qt/qtwebengine` after that.
See the
https://wiki.gentoo.org/wiki/Qutebrowser#Kerberos_authentication_does_not_work[
Troubleshooting section in Gentoo Wiki] for more information.
On Void Linux
-------------
@ -398,7 +411,7 @@ location for a particular application, rather than being installed globally.
The `scripts/mkvenv.py` script in this repository can be used to create a
virtualenv for qutebrowser and install it (including all dependencies) there.
The next couple of sections will explain the most common use-cases - run
`mkvenv.py` with `--help` to see all available options.
`scripts/mkvenv.py` with `--help` to see all available options.
Getting the repository
~~~~~~~~~~~~~~~~~~~~~~
@ -431,7 +444,7 @@ This installs all needed Python dependencies in a `.venv` subfolder
This comes with an up-to-date Qt/PyQt including a pre-compiled QtWebEngine
binary, but has a few caveats:
- Make sure your `python3` is Python 3.8 or newer, otherwise you'll get a "No
- Make sure your `python3` is Python 3.9 or newer, otherwise you'll get a "No
matching distribution found" error and/or qutebrowser will not run.
- It only works on 64-bit x86 systems, with other architectures you'll get the
same error.
@ -442,8 +455,8 @@ See the next section for an alternative install method which might help with
those issues but result in an older Qt version.
You can specify a Qt/PyQt version with the `--pyqt-version` flag, see
`mkvenv.py --help` for a list of available versions. By default, the latest
version which plays well with qutebrowser is used.
`scripts/mkvenv.py --help` for a list of available versions. By default, the
latest version which plays well with qutebrowser is used.
NOTE: If the Qt smoke test fails with a _"This application failed to start
because no Qt platform plugin could be initialized."_ message, most likely a
@ -453,22 +466,24 @@ failed on ..._ line for details.
Installing dependencies (system-wide Qt)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Alternatively, you can use `mkvenv.py --pyqt-type link` to symlink your local
PyQt/Qt install instead of installing PyQt in the virtualenv. However, unless
you have a new QtWebKit or QtWebEngine available, qutebrowser will not work. It
also typically means you'll be using an older release of QtWebEngine.
Alternatively, you can use `scripts/mkvenv.py --pyqt-type link` to symlink
your local PyQt/Qt install instead of installing PyQt in the virtualenv.
However, unless you have a new QtWebKit or QtWebEngine available, qutebrowser
will not work. It also typically means you'll be using an older release of
QtWebEngine.
On Windows, run `set PYTHON=C:\path\to\python.exe` (CMD) or `$Env:PYTHON =
"..."` (Powershell) first.
There is a third mode, `mkvenv.py --pyqt-type source` which uses a system-wide
Qt but builds PyQt from source. In most scenarios, this shouldn't be needed.
There is a third mode, `scripts/mkvenv.py --pyqt-type source` which uses a
system-wide Qt but builds PyQt from source. In most scenarios, this shouldn't
be needed.
Creating a wrapper script
~~~~~~~~~~~~~~~~~~~~~~~~~
Running `mkvenv.py` does not install a system-wide `qutebrowser` script. You can
launch qutebrowser by doing:
Running `scripts/mkvenv.py` does not install a system-wide `qutebrowser`
script. You can launch qutebrowser by doing:
----
.venv/bin/python3 -m qutebrowser
@ -485,9 +500,9 @@ You can create a simple wrapper script to start qutebrowser somewhere in your
Updating
~~~~~~~~
If you cloned the git repository, run `mkvenv.py --update` which will take care
of updating the code (via `git pull`) and recreating the environment with the
newest dependencies.
If you cloned the git repository, run `scripts/mkvenv.py --update` which will
take care of updating the code (via `git pull`) and recreating the environment
with the newest dependencies.
Alternatively, you can update your local copy of the code (e.g. by pulling the
git repo, or extracting a new version) and the virtualenv should automatically

View File

@ -137,7 +137,7 @@ var KeepReg
; Functions
Function CheckInstallation
; if there's an installed version, uninstall it first (I chose not to start the uninstaller silently, so that user sees what failed)
; if both per-user and per-machine versions are installed, unistall the one that matches $MultiUser.InstallMode
; if both per-user and per-machine versions are installed, uninstall the one that matches $MultiUser.InstallMode
StrCpy $0 ""
${if} $HasCurrentModeInstallation = 1
StrCpy $0 "$MultiUser.InstallMode"
@ -432,32 +432,18 @@ Function .onInit
StrCpy $KeepReg 1
; OS version check
${If} ${RunningX64}
; https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoa#remarks
GetWinVer $R0 Major
!if "${QT5}" == "True"
IntCmpU $R0 6 0 _os_check_fail _os_check_pass
GetWinVer $R1 Minor
IntCmpU $R1 2 _os_check_pass _os_check_fail _os_check_pass
!else
IntCmpU $R0 10 0 _os_check_fail _os_check_pass
GetWinVer $R1 Build
${If} $R1 >= 22000 ; Windows 11 21H2
Goto _os_check_pass
${ElseIf} $R1 >= 14393 ; Windows 10 1607
${AndIf} ${IsNativeAMD64} ; Windows 10 has no x86_64 emulation on arm64
Goto _os_check_pass
${EndIf}
!endif
; https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoa#remarks
; https://learn.microsoft.com/en-us/windows/release-health/release-information
; https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information
${If} ${AtLeastWin11}
Goto _os_check_pass
${ElseIf} ${IsNativeAMD64} ; Windows 10 has no x86_64 emulation on arm64
${AndIf} ${AtLeastWin10}
${AndIf} ${AtLeastBuild} 17763 ; Windows 10 1809 (also in error message below)
Goto _os_check_pass
${EndIf}
_os_check_fail:
!if "${QT5}" == "True"
MessageBox MB_OK|MB_ICONSTOP "This version of ${PRODUCT_NAME} requires a 64-bit$\r$\n\
version of Windows 8 or later."
!else
MessageBox MB_OK|MB_ICONSTOP "This version of ${PRODUCT_NAME} requires a 64-bit$\r$\n\
version of Windows 10 1607 or later."
!endif
MessageBox MB_OK|MB_ICONSTOP "This version of ${PRODUCT_NAME} requires a 64-bit$\r$\n\
version of Windows 10 1809 or later."
Abort
_os_check_pass:

View File

@ -131,9 +131,6 @@ ShowUninstDetails hide
!define /ifndef DIST_DIR ".\..\..\dist\${PRODUCT_NAME}-${VERSION}"
!endif
; If not defined, assume Qt6 (requires a more recent windows version)
!define /ifndef QT5 "False"
; Pack the exe header with upx if UPX is defined.
!ifdef UPX
!packhdr "$%TEMP%\exehead.tmp" '"upx" "--ultra-brute" "$%TEMP%\exehead.tmp"'

View File

@ -44,6 +44,11 @@
</content_rating>
<releases>
<!-- Add new releases here -->
<release version="3.4.0" date="2024-12-14"/>
<release version="3.3.1" date="2024-10-12"/>
<release version="3.3.0" date="2024-10-12"/>
<release version="3.2.1" date="2024-06-25"/>
<release version="3.2.0" date="2024-06-03"/>
<release version="3.1.0" date="2023-12-08"/>
<release version="3.0.2" date="2023-10-19"/>
<release version="3.0.1" date="2023-10-19"/>

View File

@ -48,7 +48,7 @@ Categories=Network;WebBrowser;
Exec=qutebrowser --untrusted-args %u
Terminal=false
StartupNotify=true
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/qute;
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/webp;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/qute;
Keywords=Browser
Actions=new-window;preferences;

View File

@ -25,24 +25,66 @@ INFO_PLIST_UPDATES = {
"CFBundleURLName": "local file URL",
"CFBundleURLSchemes": ["file"]
}],
'CFBundleDocumentTypes': [{
"CFBundleTypeExtensions": ["html", "htm"],
"CFBundleTypeMIMETypes": ["text/html"],
"CFBundleTypeName": "HTML document",
"CFBundleTypeOSTypes": ["HTML"],
"CFBundleTypeRole": "Viewer",
}, {
"CFBundleTypeExtensions": ["xhtml"],
"CFBundleTypeMIMETypes": ["text/xhtml"],
"CFBundleTypeName": "XHTML document",
"CFBundleTypeRole": "Viewer",
}, {
"CFBundleTypeExtensions": ["mhtml"],
"CFBundleTypeMIMETypes": ["multipart/related", "application/x-mimearchive", "message/rfc822"],
"CFBundleTypeName": "MHTML document",
"CFBundleTypeRole": "Viewer",
}],
'CFBundleDocumentTypes': [
{
"CFBundleTypeIconFile": "document.icns",
"CFBundleTypeName": name,
"CFBundleTypeRole": "Viewer",
"LSItemContentTypes": [content_type],
}
for name, content_type in [
("GIF image", "com.compuserve.gif"),
("HTML document", "public.html"),
("XHTML document", "public.xhtml"),
("JavaScript script", "com.netscape.javascript-source"),
("JPEG image", "public.jpeg"),
("MHTML document", "org.ietf.mhtml"),
("HTML5 Audio (Ogg)", "org.xiph.ogg-audio"),
("HTML5 Video (Ogg)", "org.xiph.oggv"),
("PNG image", "public.png"),
("SVG document", "public.svg-image"),
("Plain text document", "public.text"),
("HTML5 Video (WebM)", "org.webmproject.webm"),
("WebP image", "org.webmproject.webp"),
("PDF Document", "com.adobe.pdf"),
]
],
'UTImportedTypeDeclarations': [
{
"UTTypeConformsTo": ["public.data", "public.content"],
"UTTypeDescription": "MIME HTML document",
"UTTypeIconFile": "document.icns",
"UTTypeIdentifier": "org.ietf.mhtml",
"UTTypeReferenceURL": "https://www.ietf.org/rfc/rfc2557",
"UTTypeTagSpecification": {
"com.apple.ostype": "MHTM",
"public.filename-extension": ["mht", "mhtml"],
"public.mime-type": ["multipart/related", "application/x-mimearchive"],
},
},
{
"UTTypeConformsTo": ["public.audio"],
"UTTypeDescription": "Ogg Audio",
"UTTypeIconFile": "document.icns",
"UTTypeIdentifier": "org.xiph.ogg-audio",
"UTTypeReferenceURL": "https://xiph.org/ogg/",
"UTTypeTagSpecification": {
"public.filename-extension": ["ogg", "oga"],
"public.mime-type": ["audio/ogg"],
},
},
{
"UTTypeConformsTo": ["public.movie"],
"UTTypeDescription": "Ogg Video",
"UTTypeIconFile": "document.icns",
"UTTypeIdentifier": "org.xiph.ogv",
"UTTypeReferenceURL": "https://xiph.org/ogg/",
"UTTypeTagSpecification": {
"public.filename-extension": ["ogm", "ogv"],
"public.mime-type": ["video/ogg"],
},
},
],
# https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/requesting_authorization_for_media_capture_on_macos
#
# Keys based on Google Chrome's .app, except Bluetooth keys which seem to

View File

@ -1,9 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
build==1.2.1
check-manifest==0.49
importlib_metadata==7.1.0
packaging==24.0
pyproject_hooks==1.0.0
tomli==2.0.1
zipp==3.18.1
build==1.2.2.post1
check-manifest==0.50
importlib_metadata==8.6.1
packaging==24.2
pyproject_hooks==1.2.0
tomli==2.2.1
zipp==3.21.0

View File

@ -1,48 +1,73 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
backports.tarfile==1.0.0
build==1.2.1
bump2version==1.0.1
certifi==2024.2.2
cffi==1.16.0
charset-normalizer==3.3.2
cryptography==42.0.5
docutils==0.20.1
annotated-types==0.7.0
anyio==4.8.0
autocommand==2.2.2
backports.tarfile==1.2.0
bracex==2.5.post1
build==1.2.2.post1
bump-my-version==1.0.2
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
click==8.1.8
cryptography==44.0.2
docutils==0.21.2
exceptiongroup==1.2.2
github3.py==4.0.1
hunter==3.6.1
idna==3.7
importlib_metadata==7.1.0
importlib_resources==6.4.0
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
hunter==3.7.0
id==1.5.0
idna==3.10
importlib_metadata==8.6.1
importlib_resources==6.5.2
inflect==7.3.1
jaraco.classes==3.4.0
jaraco.context==5.3.0
jaraco.functools==4.0.0
jeepney==0.8.0
keyring==25.1.0
manhole==1.8.0
jaraco.collections==5.1.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jaraco.text==3.12.1
jeepney==0.9.0
keyring==25.6.0
manhole==1.8.1
markdown-it-py==3.0.0
mdurl==0.1.2
more-itertools==10.2.0
nh3==0.2.17
packaging==24.0
pkginfo==1.10.0
more-itertools==10.6.0
nh3==0.2.21
packaging==24.2
platformdirs==4.3.6
prompt_toolkit==3.0.50
pycparser==2.22
Pygments==2.17.2
PyJWT==2.8.0
Pympler==1.0.1
pyproject_hooks==1.0.0
PyQt-builder==1.16.0
pydantic==2.10.6
pydantic-settings==2.8.1
pydantic_core==2.27.2
Pygments==2.19.1
PyJWT==2.10.1
Pympler==1.1
pyproject_hooks==1.2.0
PyQt-builder==1.18.1
python-dateutil==2.9.0.post0
readme_renderer==43.0
requests==2.31.0
python-dotenv==1.0.1
questionary==2.1.0
readme_renderer==44.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==13.7.1
rich==13.9.4
rich-click==1.8.8
SecretStorage==3.3.3
sip==6.8.3
six==1.16.0
tomli==2.0.1
twine==5.0.0
typing_extensions==4.11.0
sip==6.10.0
six==1.17.0
sniffio==1.3.1
tomli==2.2.1
tomlkit==0.13.2
twine==6.1.0
typeguard==4.3.0
typing_extensions==4.12.2
uritemplate==4.1.1
# urllib3==2.2.1
zipp==3.18.1
# urllib3==2.3.0
wcmatch==10.0
wcwidth==0.2.13
zipp==3.21.0

View File

@ -1,11 +1,16 @@
hunter
pympler
github3.py
bump2version
bump-my-version
requests
pyqt-builder
build
twine
# Included to override setuptools' vendored version that is being included in
# the lock file by pip freeze.
importlib_resources
platformdirs
# Already included via test requirements
#@ ignore: urllib3

View File

@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
asciidoc==10.2.0
asciidoc==10.2.1

View File

@ -1,23 +1,23 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==23.2.0
flake8==7.0.0
flake8-bugbear==24.2.6
attrs==25.1.0
flake8==7.1.2
flake8-bugbear==24.12.12
flake8-builtins==2.5.0
flake8-comprehensions==3.14.0
flake8-comprehensions==3.16.0
flake8-debugger==4.1.2
flake8-deprecated==2.2.1
flake8-docstrings==1.7.0
flake8-future-import==0.4.7
flake8-plugin-utils==1.3.3
flake8-pytest-style==2.0.0
flake8-pytest-style==2.1.0
flake8-string-format==0.3.0
flake8-tidy-imports==4.10.0
flake8-tidy-imports==4.11.0
flake8-tuple==0.4.1
mccabe==0.7.0
pep8-naming==0.13.3
pycodestyle==2.11.1
pep8-naming==0.14.1
pycodestyle==2.12.1
pydocstyle==6.3.0
pyflakes==3.2.0
six==1.16.0
six==1.17.0
snowballstemmer==2.2.0

View File

@ -1,21 +1,18 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
chardet==5.2.0
diff_cover==9.0.0
importlib_resources==6.4.0
Jinja2==3.1.3
lxml==5.2.1
MarkupSafe==2.1.5
mypy==1.9.0
diff_cover==9.2.4
Jinja2==3.1.6
lxml==5.3.1
MarkupSafe==3.0.2
mypy==1.15.0
mypy-extensions==1.0.0
pluggy==1.4.0
Pygments==2.17.2
pluggy==1.5.0
Pygments==2.19.1
PyQt5-stubs==5.15.6.0
tomli==2.0.1
tomli==2.2.1
types-colorama==0.4.15.20240311
types-docutils==0.20.0.20240406
types-Pygments==2.17.0.20240310
types-PyYAML==6.0.12.20240311
types-setuptools==69.5.0.20240415
typing_extensions==4.11.0
zipp==3.18.1
types-docutils==0.21.0.20241128
types-Pygments==2.19.0.20250305
types-PyYAML==6.0.12.20241230
typing_extensions==4.12.2

View File

@ -6,6 +6,3 @@ PyQt5-stubs
types-PyYAML
types-colorama
types-Pygments
# So stubs are available even on newer Python versions
importlib_resources

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
altgraph==0.17.4
importlib_metadata==7.1.0
packaging==24.0
pyinstaller==6.6.0
pyinstaller-hooks-contrib==2024.4
zipp==3.18.1
importlib_metadata==8.6.1
packaging==24.2
pyinstaller==6.12.0
pyinstaller-hooks-contrib==2025.1
zipp==3.21.0

View File

@ -1,26 +1,26 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==3.1.0
certifi==2024.2.2
cffi==1.16.0
charset-normalizer==3.3.2
cryptography==42.0.5
dill==0.3.8
astroid==3.3.9
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
cryptography==44.0.2
dill==0.3.9
github3.py==4.0.1
idna==3.7
isort==5.13.2
idna==3.10
isort==6.0.1
mccabe==0.7.0
pefile==2023.2.7
platformdirs==4.2.0
pefile==2024.8.26
platformdirs==4.3.6
pycparser==2.22
PyJWT==2.8.0
pylint==3.1.0
PyJWT==2.10.1
pylint==3.3.5
python-dateutil==2.9.0.post0
./scripts/dev/pylint_checkers
requests==2.31.0
six==1.16.0
tomli==2.0.1
tomlkit==0.12.4
typing_extensions==4.11.0
requests==2.32.3
six==1.17.0
tomli==2.2.1
tomlkit==0.13.2
typing_extensions==4.12.2
uritemplate==4.1.1
# urllib3==2.2.1
# urllib3==2.3.0

View File

@ -7,7 +7,6 @@ pefile
# fix qute-pylint location
#@ replace: qute[_-]pylint.* ./scripts/dev/pylint_checkers
#@ markers: typed-ast python_version<"3.8"
# Already included via test requirements
#@ ignore: urllib3

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.2 # rq.filter: == 5.15.2
PyQt5-sip==12.13.0
PyQt5_sip==12.17.0
PyQtWebEngine==5.15.2 # rq.filter: == 5.15.2

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.10 # rq.filter: < 5.16
PyQt5-Qt5==5.15.2
PyQt5-sip==12.13.0
PyQtWebEngine==5.15.6 # rq.filter: < 5.16
PyQtWebEngine-Qt5==5.15.2
PyQt5==5.15.11 # rq.filter: < 5.16
PyQt5-Qt5==5.15.16
PyQt5_sip==12.17.0
PyQtWebEngine==5.15.7 # rq.filter: < 5.16
PyQtWebEngine-Qt5==5.15.16

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.10
PyQt5-Qt5==5.15.2
PyQt5-sip==12.13.0
PyQtWebEngine==5.15.6
PyQtWebEngine-Qt5==5.15.2
PyQt5==5.15.11
PyQt5-Qt5==5.15.16
PyQt5_sip==12.17.0
PyQtWebEngine==5.15.7
PyQtWebEngine-Qt5==5.15.16

View File

@ -2,6 +2,6 @@
PyQt6==6.2.3
PyQt6-Qt6==6.2.4
PyQt6-sip==13.6.0
PyQt6-WebEngine==6.2.1
PyQt6-WebEngine-Qt6==6.2.4
PyQt6_sip==13.10.0

View File

@ -2,6 +2,6 @@
PyQt6==6.3.1
PyQt6-Qt6==6.3.2
PyQt6-sip==13.6.0
PyQt6-WebEngine==6.3.1
PyQt6-WebEngine-Qt6==6.3.2
PyQt6_sip==13.10.0

View File

@ -2,6 +2,6 @@
PyQt6==6.4.2
PyQt6-Qt6==6.4.3
PyQt6-sip==13.6.0
PyQt6-WebEngine==6.4.0
PyQt6-WebEngine-Qt6==6.4.3
PyQt6_sip==13.10.0

View File

@ -2,6 +2,6 @@
PyQt6==6.5.3
PyQt6-Qt6==6.5.3
PyQt6-sip==13.6.0
PyQt6-WebEngine==6.5.0
PyQt6-WebEngine-Qt6==6.5.3
PyQt6_sip==13.10.0

View File

@ -2,6 +2,6 @@
PyQt6==6.6.1
PyQt6-Qt6==6.6.3
PyQt6-sip==13.6.0
PyQt6-WebEngine==6.6.0
PyQt6-WebEngine-Qt6==6.6.3
PyQt6_sip==13.10.0

View File

@ -0,0 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.7.1
PyQt6-Qt6==6.7.3
PyQt6-WebEngine==6.7.0
PyQt6-WebEngine-Qt6==6.7.3
PyQt6-WebEngineSubwheel-Qt6==6.7.3
PyQt6_sip==13.10.0

View File

@ -0,0 +1,4 @@
PyQt6 >= 6.7, < 6.8
PyQt6-Qt6 >= 6.7, < 6.8
PyQt6-WebEngine >= 6.7, < 6.8
PyQt6-WebEngine-Qt6 >= 6.7, < 6.8

View File

@ -0,0 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.8.1
PyQt6-Qt6==6.8.2
PyQt6-WebEngine==6.8.0
PyQt6-WebEngine-Qt6==6.8.2
PyQt6_sip==13.10.0

View File

@ -0,0 +1,4 @@
PyQt6 >= 6.8, < 6.9
PyQt6-Qt6 >= 6.8, < 6.9
PyQt6-WebEngine >= 6.8, < 6.9
PyQt6-WebEngine-Qt6 >= 6.8, < 6.9

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.6.1
PyQt6-Qt6==6.6.3
PyQt6-sip==13.6.0
PyQt6-WebEngine==6.6.0
PyQt6-WebEngine-Qt6==6.6.3
PyQt6==6.8.1
PyQt6-Qt6==6.8.2
PyQt6-WebEngine==6.8.0
PyQt6-WebEngine-Qt6==6.8.2
PyQt6_sip==13.10.0

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.6.1
PyQt6-Qt6==6.6.3
PyQt6-sip==13.6.0
PyQt6-WebEngine==6.6.0
PyQt6-WebEngine-Qt6==6.6.3
PyQt6==6.8.1
PyQt6-Qt6==6.8.2
PyQt6-WebEngine==6.8.0
PyQt6-WebEngine-Qt6==6.8.2
PyQt6_sip==13.10.0

View File

@ -1,17 +1,17 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
build==1.2.1
certifi==2024.2.2
charset-normalizer==3.3.2
docutils==0.20.1
idna==3.7
importlib_metadata==7.1.0
packaging==24.0
Pygments==2.17.2
pyproject_hooks==1.0.0
build==1.2.2.post1
certifi==2025.1.31
charset-normalizer==3.4.1
docutils==0.21.2
idna==3.10
importlib_metadata==8.6.1
packaging==24.2
Pygments==2.19.1
pyproject_hooks==1.2.0
pyroma==4.2
requests==2.31.0
tomli==2.0.1
trove-classifiers==2024.4.10
urllib3==2.2.1
zipp==3.18.1
requests==2.32.3
tomli==2.2.1
trove-classifiers==2025.3.3.18
urllib3==2.3.0
zipp==3.21.0

View File

@ -12,12 +12,7 @@ PyYAML
#@ add: pyobjc-core ; sys_platform=="darwin"
#@ add: pyobjc-framework-Cocoa ; sys_platform=="darwin"
## stdlib backports
importlib_resources
## Optional dependencies
Pygments # For :view-source --pygments or on QtWebKit
colorama # Colored log output on Windows
adblock # Improved adblocking
#@ markers: importlib_resources python_version=="3.8.*"

View File

@ -1,26 +1,26 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
alabaster==0.7.13
Babel==2.14.0
certifi==2024.2.2
charset-normalizer==3.3.2
docutils==0.20.1
idna==3.7
alabaster==0.7.16
babel==2.17.0
certifi==2025.1.31
charset-normalizer==3.4.1
docutils==0.21.2
idna==3.10
imagesize==1.4.1
importlib_metadata==7.1.0
Jinja2==3.1.3
MarkupSafe==2.1.5
packaging==24.0
Pygments==2.17.2
pytz==2024.1
requests==2.31.0
importlib_metadata==8.6.1
Jinja2==3.1.6
MarkupSafe==3.0.2
packaging==24.2
Pygments==2.19.1
requests==2.32.3
snowballstemmer==2.2.0
Sphinx==7.1.2
sphinxcontrib-applehelp==1.0.4
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.1
Sphinx==7.4.7
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
urllib3==2.2.1
zipp==3.18.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
tomli==2.2.1
urllib3==2.3.0
zipp==3.21.0

View File

@ -2,12 +2,13 @@
# bzr+lp:beautifulsoup
beautifulsoup4
git+https://github.com/cherrypy/cheroot.git
git+https://github.com/nedbat/coveragepy.git#egg=coverage[toml]
coverage[toml] @ git+https://github.com/nedbat/coveragepy.git
git+https://github.com/pallets/flask.git
git+https://github.com/pallets/werkzeug.git # transitive dep, but needed to work
git+https://github.com/HypothesisWorks/hypothesis.git#subdirectory=hypothesis-python
git+https://github.com/pytest-dev/pytest.git
git+https://github.com/pytest-dev/pytest-bdd.git
gherkin-official<31.0.0 # https://github.com/cucumber/gherkin/issues/373
git+https://github.com/ionelmc/pytest-benchmark.git
git+https://github.com/pytest-dev/pytest-instafail.git
git+https://github.com/pytest-dev/pytest-mock.git
@ -20,6 +21,7 @@ git+https://github.com/pygments/pygments.git
git+https://github.com/pytest-dev/pytest-repeat.git
git+https://github.com/pytest-dev/pytest-cov.git
git+https://github.com/The-Compiler/pytest-xvfb.git
git+https://github.com/python-pillow/Pillow.git
git+https://github.com/pytest-dev/pytest-xdist.git
git+https://github.com/john-kurkowski/tldextract

View File

@ -1,56 +1,67 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==23.2.0
beautifulsoup4==4.12.3
blinker==1.7.0
certifi==2024.2.2
charset-normalizer==3.3.2
cheroot==10.0.0
click==8.1.7
coverage==7.4.4
exceptiongroup==1.2.0
attrs==25.1.0
autocommand==2.2.2
backports.tarfile==1.2.0
beautifulsoup4==4.13.3
blinker==1.9.0
certifi==2025.1.31
charset-normalizer==3.4.1
cheroot==10.0.1
click==8.1.8
coverage==7.6.12
exceptiongroup==1.2.2
execnet==2.1.1
filelock==3.13.4
Flask==3.0.3
hunter==3.6.1
hypothesis==6.100.1
idna==3.7
importlib_metadata==7.1.0
filelock==3.17.0
Flask==3.1.0
gherkin-official==29.0.0
hunter==3.7.0
hypothesis==6.128.1
idna==3.10
importlib_metadata==8.6.1
importlib_resources==6.5.2
inflect==7.3.1
iniconfig==2.0.0
itsdangerous==2.1.2
jaraco.functools==4.0.0
# Jinja2==3.1.3
Mako==1.3.3
manhole==1.8.0
# MarkupSafe==2.1.5
more-itertools==10.2.0
packaging==24.0
parse==1.20.1
parse-type==0.6.2
pluggy==1.4.0
itsdangerous==2.2.0
jaraco.collections==5.1.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jaraco.text==3.12.1
# Jinja2==3.1.6
Mako==1.3.9
manhole==1.8.1
# MarkupSafe==3.0.2
more-itertools==10.6.0
packaging==24.2
parse==1.20.2
parse_type==0.6.4
pillow==11.1.0
platformdirs==4.3.6
pluggy==1.5.0
py-cpuinfo==9.0.0
Pygments==2.17.2
pytest==8.1.1
pytest-bdd==7.1.2
pytest-benchmark==4.0.0
pytest-cov==5.0.0
Pygments==2.19.1
pytest==8.3.5
pytest-bdd==8.1.0
pytest-benchmark==5.1.0
pytest-cov==6.0.0
pytest-instafail==0.5.0
pytest-mock==3.14.0
pytest-qt==4.4.0
pytest-repeat==0.9.3
pytest-rerunfailures==14.0
pytest-xdist==3.5.0
pytest-xvfb==3.0.0
pytest-rerunfailures==15.0
pytest-xdist==3.6.1
pytest-xvfb==3.1.1
PyVirtualDisplay==3.0
requests==2.31.0
requests-file==2.0.0
six==1.16.0
requests==2.32.3
requests-file==2.1.0
six==1.17.0
sortedcontainers==2.4.0
soupsieve==2.5
tldextract==5.1.2
tomli==2.0.1
typing_extensions==4.11.0
urllib3==2.2.1
vulture==2.11
Werkzeug==3.0.2
zipp==3.18.1
soupsieve==2.6
tldextract==5.1.3
tomli==2.2.1
typeguard==4.3.0
typing_extensions==4.12.2
urllib3==2.3.0
vulture==2.14
Werkzeug==3.1.3
zipp==3.21.0

View File

@ -25,10 +25,21 @@ pytest-cov
# To avoid windows from popping up
pytest-xvfb
PyVirtualDisplay
pillow
# To run on multiple cores with -n
pytest-xdist
# Needed to test misc/userscripts/qute-lastpass
tldextract
# importlib_resources==6.4.0, jaraco.context and platformdirs are being
# included in the lock file via setuptools' vendored dependencies and
# conflicting with the more up to date one pulled down by other requirements
# files.
# Include them here even though we don't need them to make sure we at least
# get an up to date version.
importlib_resources
jaraco.context
platformdirs
#@ ignore: Jinja2, MarkupSafe, colorama

View File

@ -1,17 +1,18 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
cachetools==5.3.3
cachetools==5.5.2
chardet==5.2.0
colorama==0.4.6
distlib==0.3.8
filelock==3.13.4
packaging==24.0
pip==24.0
platformdirs==4.2.0
pluggy==1.4.0
pyproject-api==1.6.1
setuptools==69.5.1
tomli==2.0.1
tox==4.14.2
virtualenv==20.25.1
wheel==0.43.0
distlib==0.3.9
filelock==3.17.0
packaging==24.2
pip==25.0.1
platformdirs==4.3.6
pluggy==1.5.0
pyproject-api==1.9.0
setuptools==76.0.0
tomli==2.2.1
tox==4.24.2
typing_extensions==4.12.2
virtualenv==20.29.3
wheel==0.45.1

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
tomli==2.0.1
vulture==2.11
tomli==2.2.1
vulture==2.14

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
pathspec==0.12.1
PyYAML==6.0.1
PyYAML==6.0.2
yamllint==1.35.1

View File

@ -106,6 +106,8 @@ The following userscripts can be found on their own repositories.
More powerfully manage single window sessions
- [qutebrowser-url-mutator](https://codeberg.org/mister_monster/qutebrowser-url-mutator):
automatically mutates input URLs based on configurable rules
- [qute-translate-popup](https://github.com/JohnBardoe/qute-translate-popup):
selected text translation, with a qute popup!
[Zotero]: https://www.zotero.org/
[Pocket]: https://getpocket.com/

View File

@ -96,7 +96,8 @@ def ask_password(password_prompt_invocation):
raise Exception('Could not unlock vault')
master_pass = process.stdout.strip()
return subprocess.check_output(
['bw', 'unlock', '--raw', master_pass],
['bw', 'unlock', '--raw', '--passwordenv', 'BW_MASTERPASS'],
env={**os.environ, 'BW_MASTERPASS': master_pass},
text=True,
).strip()

View File

@ -40,11 +40,13 @@ import argparse
import enum
import fnmatch
import functools
import idna
import os
import re
import shlex
import subprocess
import sys
import unicodedata
from urllib.parse import urlparse
import tldextract
@ -116,6 +118,23 @@ def qute_command(command):
fifo.write(command + '\n')
fifo.flush()
# Encode candidate string parts as Internationalized Domain Name, doing
# Unicode normalization before. This allows to properly match (non-ASCII)
# pass entries with the corresponding domain names.
def idna_encode(name):
# Do Unicode normalization first, we use form NFKC because:
# 1. Use the compatibility normalization because these sequences have "the same meaning in some contexts"
# 2. idna.encode() below requires the Unicode strings to be in normalization form C
# See https://en.wikipedia.org/wiki/Unicode_equivalence#Normal_forms
unicode_normalized = unicodedata.normalize("NFKC", name)
# Empty strings can not be encoded, they appear for example as empty
# parts in split_path. If something like this happens, we just fall back
# to the unicode representation (which may already be ASCII then).
try:
idna_encoded = idna.encode(unicode_normalized)
except idna.IDNAError:
idna_encoded = unicode_normalized
return idna_encoded
def find_pass_candidates(domain, unfiltered=False):
candidates = []
@ -130,6 +149,7 @@ def find_pass_candidates(domain, unfiltered=False):
if unfiltered or domain in password:
candidates.append(password)
else:
idna_domain = idna_encode(domain)
for path, directories, file_names in os.walk(arguments.password_store, followlinks=True):
secrets = fnmatch.filter(file_names, '*.gpg')
if not secrets:
@ -138,11 +158,14 @@ def find_pass_candidates(domain, unfiltered=False):
# Strip password store path prefix to get the relative pass path
pass_path = path[len(arguments.password_store):]
split_path = pass_path.split(os.path.sep)
idna_split_path = [idna_encode(part) for part in split_path]
for secret in secrets:
secret_base = os.path.splitext(secret)[0]
if not unfiltered and domain not in (split_path + [secret_base]):
idna_secret_base = idna_encode(secret_base)
if not unfiltered and idna_domain not in (idna_split_path + [idna_secret_base]):
continue
# Append the unencoded Unicode path/name since this is how pass uses them
candidates.append(os.path.join(pass_path, secret_base))
return candidates

View File

@ -9,18 +9,16 @@
# :spawn --userscript ripbang amazon maps
#
import os, re, requests, sys
from urllib.parse import urlparse, parse_qs
import os, requests, sys
for argument in sys.argv[1:]:
bang = '!' + argument
r = requests.get('https://duckduckgo.com/',
r = requests.get('https://html.duckduckgo.com/html/',
allow_redirects=False,
params={'q': bang + ' SEARCHTEXT'},
headers={'user-agent': 'qutebrowser ripbang'})
searchengine = re.search("url=([^']+)", r.text).group(1)
searchengine = urlparse(searchengine).query
searchengine = parse_qs(searchengine)['uddg'][0]
searchengine = r.headers['location']
searchengine = searchengine.replace('SEARCHTEXT', '{}')
if os.getenv('QUTE_FIFO'):

View File

@ -19,6 +19,7 @@ markers =
not_frozen: Tests which can't be run if sys.frozen is True.
not_flatpak: Tests which can't be run if running with Flatpak.
no_xvfb: Tests which can't be run with Xvfb.
no_offscreen: Tests which can't be run with the offscreen platform plugin.
frozen: Tests which can only be run if sys.frozen is True.
integration: Tests which test a bigger portion of code
end2end: End to end tests which run qutebrowser as subprocess
@ -41,6 +42,7 @@ markers =
qt6_only: Tests which should only run with Qt 6
qt5_xfail: Tests which fail with Qt 5
qt6_xfail: Tests which fail with Qt 6
qt69_ci_flaky: Tests which are flaky with Qt 6.9 on CI
qt_log_level_fail = WARNING
qt_log_ignore =
# GitHub Actions
@ -70,12 +72,28 @@ qt_log_ignore =
# The last part of the outer message gets bumped down to a line on its own, so hopefully this
# catches that. And we don't see any other weird permutations of this.
^[^ ]*qtwebengine_dictionaries'$
# Qt 5 on Archlinux
^QSslSocket: cannot resolve .*
# Seems to happen after we try to complete immediately after clearing a
# model, for example, when no completion function is available for the
# current text pattern.
QItemSelectionModel: Selecting when no model has been set will result in a no-op.
^QSaveFile::commit: File \(.*[/\\]test_failing_flush0[/\\]foo\) is not open$
^The following paths were searched for Qt WebEngine dictionaries:.*
# Qt 6.9 with Xvfb
^Backend texture is not a Vulkan texture\.$
^Compositor returned null texture$
# With offscreen platform plugin
^This plugin does not support (raise\(\)|propagateSizeHints\(\)|createPlatformVulkanInstance|grabbing the keyboard)$
^QRhiGles2: Failed to create (temporary )?context$
^QVulkanInstance: Failed to initialize Vulkan$
^Unable to detect GPU vendor\.$
xfail_strict = true
filterwarnings =
error
default:Test process .* failed to terminate!:UserWarning
# Python 3.12: https://github.com/jendrikseipp/vulture/issues/314
ignore:ast\.Str is deprecated and will be removed in Python 3\.14; use ast\.Constant instead:DeprecationWarning:vulture\.core
# Python 3.12: https://github.com/ionelmc/pytest-benchmark/issues/240
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
# https://github.com/cucumber/gherkin/commit/2f4830093149eae7ff7bd82f683b3d3bb7320d39
# https://github.com/pytest-dev/pytest-bdd/issues/752
ignore:'maxsplit' is passed as positional argument:DeprecationWarning:gherkin.gherkin_line
faulthandler_timeout = 90
xvfb_colordepth = 24

View File

@ -5,13 +5,16 @@
"""A keyboard-driven, vim-like browser based on Python and Qt."""
import os.path
import datetime
_year = datetime.date.today().year
__author__ = "Florian Bruhin"
__copyright__ = "Copyright 2014-2021 Florian Bruhin (The Compiler)"
__copyright__ = "Copyright 2013-{} Florian Bruhin (The Compiler)".format(_year)
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version__ = "3.1.0"
__version__ = "3.4.0"
__version_info__ = tuple(int(part) for part in __version__.split('.'))
__description__ = "A keyboard-driven, vim-like browser based on Python and Qt."

View File

@ -35,7 +35,8 @@ Possible values:
import inspect
from typing import Any, Callable, Iterable, Protocol, Optional, Dict, cast
from typing import Any, Protocol, Optional, cast
from collections.abc import Iterable, Callable
from qutebrowser.utils import qtutils
from qutebrowser.commands import command, cmdexc
@ -101,7 +102,7 @@ class _CmdHandlerType(Protocol):
Below, we cast the decorated function to _CmdHandlerType to make mypy aware of this.
"""
qute_args: Optional[Dict[str, 'command.ArgInfo']]
qute_args: Optional[dict[str, 'command.ArgInfo']]
def __call__(self, *args: Any, **kwargs: Any) -> Any:
...

View File

@ -7,7 +7,8 @@
"""Hooks for extensions."""
import importlib
from typing import Callable, Any
from typing import Any
from collections.abc import Callable
from qutebrowser.extensions import loader

View File

@ -29,7 +29,8 @@ import tempfile
import pathlib
import datetime
import argparse
from typing import Iterable, Optional, List, Tuple
from typing import Optional
from collections.abc import Iterable
from qutebrowser.qt import machinery
from qutebrowser.qt.widgets import QApplication, QWidget
@ -330,7 +331,7 @@ def _open_special_pages(args):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
pages: List[Tuple[str, bool, str]] = [
pages: list[tuple[str, bool, str]] = [
# state, condition, URL
('quickstart-done',
True,

View File

@ -9,8 +9,8 @@ import pathlib
import itertools
import functools
import dataclasses
from typing import (cast, TYPE_CHECKING, Any, Callable, Iterable, List, Optional,
Sequence, Set, Type, Union, Tuple)
from typing import (cast, TYPE_CHECKING, Any, Optional, Union)
from collections.abc import Iterable, Sequence, Callable
from qutebrowser.qt import machinery
from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt,
@ -62,7 +62,7 @@ def create(win_id: int,
mode_manager = modeman.instance(win_id)
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab
tab_class: Type[AbstractTab] = webenginetab.WebEngineTab
tab_class: type[AbstractTab] = webenginetab.WebEngineTab
elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkittab
tab_class = webkittab.WebKitTab
@ -144,7 +144,7 @@ class AbstractAction:
"""Attribute ``action`` of AbstractTab for Qt WebActions."""
action_base: Type[Union['QWebPage.WebAction', 'QWebEnginePage.WebAction']]
action_base: type[Union['QWebPage.WebAction', 'QWebEnginePage.WebAction']]
def __init__(self, tab: 'AbstractTab') -> None:
self._widget = cast(_WidgetType, None)
@ -641,7 +641,7 @@ class AbstractScroller(QObject):
def pos_px(self) -> QPoint:
raise NotImplementedError
def pos_perc(self) -> Tuple[int, int]:
def pos_perc(self) -> tuple[int, int]:
raise NotImplementedError
def to_perc(self, x: float = None, y: float = None) -> None:
@ -767,10 +767,10 @@ class AbstractHistory:
def _go_to_item(self, item: Any) -> None:
raise NotImplementedError
def back_items(self) -> List[Any]:
def back_items(self) -> list[Any]:
raise NotImplementedError
def forward_items(self) -> List[Any]:
def forward_items(self) -> list[Any]:
raise NotImplementedError
@ -1020,7 +1020,7 @@ class AbstractTab(QWidget):
# Note that we remember hosts here, without scheme/port:
# QtWebEngine/Chromium also only remembers hostnames, and certificates are
# for a given hostname anyways.
_insecure_hosts: Set[str] = set()
_insecure_hosts: set[str] = set()
# Sub-APIs initialized by subclasses
history: AbstractHistory

View File

@ -2,13 +2,16 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
# pylint: disable=too-many-positional-arguments
"""Command dispatcher for TabbedBrowser."""
import os.path
import shlex
import functools
import urllib.parse
from typing import cast, Callable, Dict, Union, Optional
from typing import cast, Union, Optional
from collections.abc import Callable
from qutebrowser.qt.widgets import QApplication, QTabBar
from qutebrowser.qt.core import Qt, QUrl, QEvent, QUrlQuery
@ -709,7 +712,7 @@ class CommandDispatcher:
widget = self._current_widget()
url = self._current_url()
handlers: Dict[str, Callable[..., QUrl]] = {
handlers: dict[str, Callable[..., QUrl]] = {
'prev': functools.partial(navigate.prevnext, prev=True),
'next': functools.partial(navigate.prevnext, prev=False),
'up': navigate.path_up,
@ -771,28 +774,6 @@ class CommandDispatcher:
"Numeric argument is too large for internal int "
"representation.")
def _yank_url(self, what):
"""Helper method for yank() to get the URL to copy."""
assert what in ['url', 'pretty-url'], what
if what == 'pretty-url':
flags = urlutils.FormatOption.DECODE_RESERVED
else:
flags = urlutils.FormatOption.ENCODED
flags |= urlutils.FormatOption.REMOVE_PASSWORD
url = QUrl(self._current_url())
url_query = QUrlQuery()
url_query_str = url.query()
if '&' not in url_query_str and ';' in url_query_str:
url_query.setQueryDelimiters('=', ';')
url_query.setQuery(url_query_str)
for key in dict(url_query.queryItems()):
if key in config.val.url.yank_ignored_parameters:
url_query.removeQueryItem(key)
url.setQuery(url_query)
return url.toString(flags)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('what', choices=['selection', 'url', 'pretty-url',
'title', 'domain', 'inline'])
@ -827,7 +808,9 @@ class CommandDispatcher:
self._current_url().host(),
':' + str(port) if port > -1 else '')
elif what in ['url', 'pretty-url']:
s = self._yank_url(what)
url = self._current_url()
pretty = what == 'pretty-url'
s = urlutils.get_url_yank_text(url, pretty=pretty)
what = 'URL' # For printing
elif what == 'selection':
def _selection_callback(s):
@ -1057,6 +1040,8 @@ class CommandDispatcher:
"No window specified and couldn't find active window!")
assert isinstance(active_win, mainwindow.MainWindow), active_win
win_id = active_win.win_id
else:
raise utils.Unreachable(index_parts)
if win_id not in objreg.window_registry:
raise cmdutils.CommandError(
@ -1325,8 +1310,7 @@ class CommandDispatcher:
else:
cmd = os.path.expanduser(cmd)
proc = guiprocess.GUIProcess(what='command', verbose=verbose,
output_messages=output_messages,
parent=self._tabbed_browser)
output_messages=output_messages)
if detach:
ok = proc.start_detached(cmd, args)
if not ok:

View File

@ -13,7 +13,8 @@ import functools
import pathlib
import tempfile
import enum
from typing import Any, Dict, IO, List, MutableSequence, Optional, Union
from typing import Any, IO, Optional, Union
from collections.abc import MutableSequence
from qutebrowser.qt.core import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
QTimer, QAbstractListModel, QUrl)
@ -187,15 +188,22 @@ def transform_path(path):
"""
if not utils.is_windows:
return path
path = utils.expand_windows_drive(path)
# Drive dependent working directories are not supported, e.g.
# E:filename is invalid
if re.search(r'^[A-Z]:[^\\]', path, re.IGNORECASE):
return None
# Paths like COM1, ...
# See https://github.com/qutebrowser/qutebrowser/issues/82
if pathlib.Path(path).is_reserved():
return None
if sys.version_info[:2] >= (3, 13):
if os.path.isreserved(path): # pylint: disable=no-member
return None
else:
if pathlib.Path(path).is_reserved(): # pylint: disable=else-if-used
return None
return path
@ -447,7 +455,7 @@ class AbstractDownloadItem(QObject):
UnsupportedAttribute, IO[bytes], None
] = UnsupportedAttribute()
self.raw_headers: Union[
UnsupportedAttribute, Dict[bytes, bytes]
UnsupportedAttribute, dict[bytes, bytes]
] = UnsupportedAttribute()
self._filename: Optional[str] = None
@ -899,7 +907,7 @@ class AbstractDownloadManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.downloads: List[AbstractDownloadItem] = []
self.downloads: list[AbstractDownloadItem] = []
self._update_timer = usertypes.Timer(self, 'download-update')
self._update_timer.timeout.connect(self._update_gui)
self._update_timer.setInterval(_REFRESH_INTERVAL)
@ -1264,7 +1272,7 @@ class DownloadModel(QAbstractListModel):
else:
return ""
def data(self, index, role):
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
"""Download data from DownloadManager."""
if not index.isValid():
return None

View File

@ -5,7 +5,8 @@
"""The ListView to display downloads in."""
import functools
from typing import Callable, MutableSequence, Tuple, Union
from typing import Union
from collections.abc import MutableSequence, Callable
from qutebrowser.qt.core import pyqtSlot, QSize, Qt
from qutebrowser.qt.widgets import QListView, QSizePolicy, QMenu, QStyleFactory
@ -17,8 +18,8 @@ from qutebrowser.utils import qtutils, utils
_ActionListType = MutableSequence[
Union[
Tuple[None, None], # separator
Tuple[str, Callable[[], None]],
tuple[None, None], # separator
tuple[str, Callable[[], None]],
]
]

View File

@ -12,7 +12,8 @@ import functools
import glob
import textwrap
import dataclasses
from typing import cast, List, Sequence, Tuple, Optional
from typing import cast, Optional
from collections.abc import Sequence
from qutebrowser.qt.core import pyqtSignal, QObject, QUrl
@ -207,9 +208,9 @@ class MatchingScripts:
"""All userscripts registered to run on a particular url."""
url: QUrl
start: List[GreasemonkeyScript] = dataclasses.field(default_factory=list)
end: List[GreasemonkeyScript] = dataclasses.field(default_factory=list)
idle: List[GreasemonkeyScript] = dataclasses.field(default_factory=list)
start: list[GreasemonkeyScript] = dataclasses.field(default_factory=list)
end: list[GreasemonkeyScript] = dataclasses.field(default_factory=list)
idle: list[GreasemonkeyScript] = dataclasses.field(default_factory=list)
@dataclasses.dataclass
@ -217,8 +218,8 @@ class LoadResults:
"""The results of loading all Greasemonkey scripts."""
successful: List[GreasemonkeyScript] = dataclasses.field(default_factory=list)
errors: List[Tuple[str, str]] = dataclasses.field(default_factory=list)
successful: list[GreasemonkeyScript] = dataclasses.field(default_factory=list)
errors: list[tuple[str, str]] = dataclasses.field(default_factory=list)
def successful_str(self) -> str:
"""Get a string with all successfully loaded scripts.
@ -294,10 +295,10 @@ class GreasemonkeyManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._run_start: List[GreasemonkeyScript] = []
self._run_end: List[GreasemonkeyScript] = []
self._run_idle: List[GreasemonkeyScript] = []
self._in_progress_dls: List[downloads.AbstractDownloadItem] = []
self._run_start: list[GreasemonkeyScript] = []
self._run_end: list[GreasemonkeyScript] = []
self._run_idle: list[GreasemonkeyScript] = []
self._in_progress_dls: list[downloads.AbstractDownloadItem] = []
def load_scripts(self, *, force: bool = False) -> LoadResults:
"""Re-read Greasemonkey scripts from disk.

View File

@ -12,8 +12,15 @@ import html
import enum
import dataclasses
from string import ascii_lowercase
from typing import (TYPE_CHECKING, Callable, Dict, Iterable, Iterator, List, Mapping,
MutableSequence, Optional, Sequence, Set)
from typing import (TYPE_CHECKING, Optional)
from collections.abc import (
Iterable,
Iterator,
Mapping,
MutableSequence,
Sequence,
Callable,
)
from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, Qt, QUrl
from qutebrowser.qt.widgets import QLabel
@ -175,11 +182,11 @@ class HintContext:
add_history: bool
first: bool
baseurl: QUrl
args: List[str]
args: list[str]
group: str
all_labels: List[HintLabel] = dataclasses.field(default_factory=list)
labels: Dict[str, HintLabel] = dataclasses.field(default_factory=dict)
all_labels: list[HintLabel] = dataclasses.field(default_factory=list)
labels: dict[str, HintLabel] = dataclasses.field(default_factory=dict)
to_follow: Optional[str] = None
first_run: bool = True
filterstr: Optional[str] = None
@ -237,11 +244,7 @@ class HintActions:
sel = (context.target == Target.yank_primary and
utils.supports_selection())
flags = urlutils.FormatOption.ENCODED | urlutils.FormatOption.REMOVE_PASSWORD
if url.scheme() == 'mailto':
flags |= urlutils.FormatOption.REMOVE_SCHEME
urlstr = url.toString(flags)
urlstr = urlutils.get_url_yank_text(url, pretty=False)
new_content = urlstr
# only second and consecutive yanks are to append to the clipboard
@ -1037,7 +1040,7 @@ class WordHinter:
def __init__(self) -> None:
# will be initialized on first use.
self.words: Set[str] = set()
self.words: set[str] = set()
self.dictionary = None
def ensure_initialized(self) -> None:
@ -1147,7 +1150,7 @@ class WordHinter:
"""
self.ensure_initialized()
hints = []
used_hints: Set[str] = set()
used_hints: set[str] = set()
words = iter(self.words)
for elem in elems:
hint = self.new_hint_for(elem, used_hints, words)

View File

@ -8,7 +8,8 @@ import os
import time
import contextlib
import pathlib
from typing import cast, Mapping, MutableSequence, Optional
from typing import cast, Optional
from collections.abc import Mapping, MutableSequence
from qutebrowser.qt import machinery
from qutebrowser.qt.core import pyqtSlot, QUrl, QObject, pyqtSignal

View File

@ -6,7 +6,7 @@
import re
import posixpath
from typing import Optional, Set
from typing import Optional
from qutebrowser.qt.core import QUrl
@ -79,7 +79,7 @@ def incdec(url, count, inc_or_dec):
inc_or_dec: Either 'increment' or 'decrement'.
"""
urlutils.ensure_valid(url)
segments: Optional[Set[str]] = (
segments: Optional[set[str]] = (
set(config.val.url.incdec_segments)
)

View File

@ -242,7 +242,7 @@ class PACFetcher(QObject):
pac_prefix = "pac+"
assert url.scheme().startswith(pac_prefix)
url.setScheme(url.scheme()[len(pac_prefix):])
url.setScheme(url.scheme().removeprefix(pac_prefix))
self._pac_url = url
with qtlog.disable_qt_msghandler():

View File

@ -7,7 +7,7 @@
from typing import Optional
from qutebrowser.qt.core import QUrl, pyqtSlot
from qutebrowser.qt.network import QNetworkProxy, QNetworkProxyFactory
from qutebrowser.qt.network import QNetworkProxy, QNetworkProxyFactory, QNetworkProxyQuery
from qutebrowser.config import config, configtypes
from qutebrowser.utils import message, usertypes, urlutils, utils, qtutils
@ -71,7 +71,7 @@ class ProxyFactory(QNetworkProxyFactory):
capabilities &= ~lookup_cap
proxy.setCapabilities(capabilities)
def queryProxy(self, query):
def queryProxy(self, query: QNetworkProxyQuery = QNetworkProxyQuery()) -> list[QNetworkProxy]:
"""Get the QNetworkProxies for a query.
Args:

View File

@ -69,6 +69,10 @@ def generate_pdfjs_page(filename, url):
return html
def _get_polyfills() -> str:
return resources.read_file("javascript/pdfjs_polyfills.js")
def _generate_pdfjs_script(filename):
"""Generate the script that shows the pdf with pdf.js.
@ -83,6 +87,8 @@ def _generate_pdfjs_script(filename):
js_url = javascript.to_js(url.toString(urlutils.FormatOption.ENCODED))
return jinja.js_environment.from_string("""
{{ polyfills }}
document.addEventListener("DOMContentLoaded", function() {
if (typeof window.PDFJS !== 'undefined') {
// v1.x
@ -104,7 +110,7 @@ def _generate_pdfjs_script(filename):
});
}
});
""").render(url=js_url)
""").render(url=js_url, polyfills=_get_polyfills())
def get_pdfjs_res_and_path(path):
@ -148,6 +154,14 @@ def get_pdfjs_res_and_path(path):
log.misc.warning("OSError while reading PDF.js file: {}".format(e))
raise PDFJSNotFound(path) from None
if path == "build/pdf.worker.mjs":
content = b"\n".join(
[
_get_polyfills().encode("ascii"),
content,
]
)
return content, file_path

View File

@ -9,7 +9,7 @@ import os.path
import shutil
import functools
import dataclasses
from typing import Dict, IO, Optional
from typing import IO, Optional
from qutebrowser.qt.core import pyqtSlot, pyqtSignal, QTimer, QUrl
from qutebrowser.qt.widgets import QApplication
@ -73,7 +73,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
"""
super().__init__(manager=manager, parent=manager)
self.fileobj: Optional[IO[bytes]] = None
self.raw_headers: Dict[bytes, bytes] = {}
self.raw_headers: dict[bytes, bytes] = {}
self._autoclose = True
self._retry_info = None
@ -124,7 +124,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
log.downloads.exception("Error while closing file object")
if pos == 0:
# Emtpy remaining file
# Empty remaining file
filename = self._get_open_filename()
log.downloads.debug(f"Removing empty file at {filename}")
try:

View File

@ -18,7 +18,8 @@ import textwrap
import urllib
import collections
import secrets
from typing import TypeVar, Callable, Dict, List, Optional, Union, Sequence, Tuple
from typing import TypeVar, Optional, Union
from collections.abc import Sequence, Callable
from qutebrowser.qt.core import QUrlQuery, QUrl
@ -35,7 +36,7 @@ pyeval_output = ":pyeval was never called"
csrf_token: Optional[str] = None
_HANDLERS: Dict[str, "_HandlerCallable"] = {}
_HANDLERS: dict[str, "_HandlerCallable"] = {}
class Error(Exception):
@ -77,7 +78,7 @@ class Redirect(Exception):
# Return value: (mimetype, data) (encoded as utf-8 if a str is returned)
_HandlerRet = Tuple[str, Union[str, bytes]]
_HandlerRet = tuple[str, Union[str, bytes]]
_HandlerCallable = Callable[[QUrl], _HandlerRet]
_Handler = TypeVar('_Handler', bound=_HandlerCallable)
@ -105,7 +106,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
return self._function(url)
def data_for_url(url: QUrl) -> Tuple[str, bytes]:
def data_for_url(url: QUrl) -> tuple[str, bytes]:
"""Get the data to show for the given URL.
Args:
@ -180,7 +181,7 @@ def qute_bookmarks(_url: QUrl) -> _HandlerRet:
@add_handler('tabs')
def qute_tabs(_url: QUrl) -> _HandlerRet:
"""Handler for qute://tabs. Display information about all open tabs."""
tabs: Dict[str, List[Tuple[str, str]]] = collections.defaultdict(list)
tabs: dict[str, list[tuple[str, str]]] = collections.defaultdict(list)
for win_id, window in objreg.window_registry.items():
if sip.isdeleted(window):
continue
@ -201,7 +202,7 @@ def qute_tabs(_url: QUrl) -> _HandlerRet:
def history_data(
start_time: float,
offset: int = None
) -> Sequence[Dict[str, Union[str, int]]]:
) -> Sequence[dict[str, Union[str, int]]]:
"""Return history data.
Arguments:

View File

@ -10,11 +10,12 @@ import html
import enum
import netrc
import tempfile
from typing import Callable, Mapping, List, Optional, Iterable, Iterator
from typing import Optional
from collections.abc import Mapping, Iterable, Iterator, Callable
from qutebrowser.qt.core import QUrl, pyqtBoundSignal
from qutebrowser.config import config
from qutebrowser.config import config, configtypes
from qutebrowser.utils import (usertypes, message, log, objreg, jinja, utils,
qtutils, version, urlutils)
from qutebrowser.mainwindow import mainwindow
@ -25,8 +26,15 @@ class CallSuper(Exception):
"""Raised when the caller should call the superclass instead."""
def custom_headers(url):
"""Get the combined custom headers."""
def custom_headers(
url: QUrl, *, fallback_accept_language: bool = True
) -> list[tuple[bytes, bytes]]:
"""Get the combined custom headers.
Arguments:
fallback_accept_language: Whether to include the global (rather than
per-domain override) accept language header as well.
"""
headers = {}
dnt_config = config.instance.get('content.headers.do_not_track', url=url)
@ -40,9 +48,17 @@ def custom_headers(url):
encoded_value = b"" if value is None else value.encode('ascii')
headers[encoded_header] = encoded_value
# On QtWebEngine, we have fallback_accept_language set to False here for XHR
# requests, so that we don't end up overriding headers that are set via the XHR API.
#
# The global Accept-Language header is set via
# QWebEngineProfile::setHttpAcceptLanguage already anyways, so we only need
# to take care of URL pattern overrides here.
#
# note: Once we drop QtWebKit, we could hardcode fallback_accept_language to False.
accept_language = config.instance.get('content.headers.accept_language',
url=url)
if accept_language is not None:
url=url, fallback=fallback_accept_language)
if accept_language is not None and not isinstance(accept_language, usertypes.Unset):
headers[b'Accept-Language'] = accept_language.encode('ascii')
return sorted(headers.items())
@ -303,6 +319,7 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on,
None otherwise.
"""
config_val = config.instance.get(option, url=url)
opt = config.instance.get_opt(option)
if config_val == 'ask':
if url.isValid():
urlstr = url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
@ -328,12 +345,21 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on,
cancel_action=no_action, abort_on=abort_on,
title='Permission request', text=text, url=urlstr,
option=option)
elif config_val:
if isinstance(opt.typ, configtypes.AsBool):
config_val = opt.typ.to_bool(config_val)
if config_val is True:
yes_action()
return None
else:
elif config_val is False:
no_action()
return None
else:
raise AssertionError(
f"Unsupported value for permission prompt setting ({option}), expected boolean or "
f"'ask', got: {config_val} ({type(config_val)})"
)
def get_tab(win_id, target):
@ -445,7 +471,7 @@ class FileSelectionMode(enum.Enum):
folder = enum.auto()
def choose_file(qb_mode: FileSelectionMode) -> List[str]:
def choose_file(qb_mode: FileSelectionMode) -> list[str]:
"""Select file(s)/folder for up-/downloading, using an external command.
Args:
@ -485,10 +511,10 @@ def choose_file(qb_mode: FileSelectionMode) -> List[str]:
def _execute_fileselect_command(
command: List[str],
command: list[str],
qb_mode: FileSelectionMode,
tmpfilename: Optional[str] = None
) -> List[str]:
) -> list[str]:
"""Execute external command to choose file.
Args:
@ -522,7 +548,7 @@ def _execute_fileselect_command(
def _validated_selected_files(
qb_mode: FileSelectionMode,
selected_files: List[str],
selected_files: list[str],
) -> Iterator[str]:
"""Validates selected files if they are.

View File

@ -15,7 +15,7 @@ import os.path
import html
import functools
import collections
from typing import MutableMapping
from collections.abc import MutableMapping
from qutebrowser.qt.core import pyqtSignal, QUrl, QObject

View File

@ -4,7 +4,8 @@
"""Generic web element related code."""
from typing import Iterator, Optional, Set, TYPE_CHECKING, Union, Dict
from typing import Optional, TYPE_CHECKING, Union
from collections.abc import Iterator
import collections.abc
from qutebrowser.qt import machinery
@ -22,9 +23,9 @@ if TYPE_CHECKING:
JsValueType = Union[int, float, str, None]
if machinery.IS_QT6:
KeybordModifierType = Qt.KeyboardModifier
KeyboardModifierType = Qt.KeyboardModifier
else:
KeybordModifierType = Union[Qt.KeyboardModifiers, Qt.KeyboardModifier]
KeyboardModifierType = Union[Qt.KeyboardModifiers, Qt.KeyboardModifier]
class Error(Exception):
@ -93,7 +94,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
"""Get the geometry for this element."""
raise NotImplementedError
def classes(self) -> Set[str]:
def classes(self) -> set[str]:
"""Get a set of classes assigned to this element."""
raise NotImplementedError
@ -336,7 +337,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
log.webelem.debug("Sending fake click to {!r} at position {} with "
"target {}".format(self, pos, click_target))
target_modifiers: Dict[usertypes.ClickTarget, KeybordModifierType] = {
target_modifiers: dict[usertypes.ClickTarget, KeyboardModifierType] = {
usertypes.ClickTarget.normal: Qt.KeyboardModifier.NoModifier,
usertypes.ClickTarget.window: Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.ShiftModifier,
usertypes.ClickTarget.tab: Qt.KeyboardModifier.ControlModifier,
@ -355,10 +356,14 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
QMouseEvent(QEvent.Type.MouseButtonRelease, pos, button, Qt.MouseButton.NoButton, modifiers),
]
for evt in events:
self._tab.send_event(evt)
def _send_events_after_delay() -> None:
"""Delay clicks to workaround timing issue in e2e tests on 6.7."""
for evt in events:
self._tab.send_event(evt)
QTimer.singleShot(0, self._move_text_cursor)
QTimer.singleShot(0, self._move_text_cursor)
QTimer.singleShot(10, _send_events_after_delay)
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
"""Fake a click on an editable input field."""

View File

@ -113,6 +113,11 @@ Qt 6.6
- New alternative image classifier:
https://chromium-review.googlesource.com/c/chromium/src/+/3987823
Qt 6.7
------
Enabling dark mode can now be done at runtime via QWebEngineSettings.
"""
import os
@ -120,12 +125,16 @@ import copy
import enum
import dataclasses
import collections
from typing import (Any, Iterator, Mapping, MutableMapping, Optional, Set, Tuple, Union,
Sequence, List)
from typing import (Any, Optional, Union)
from collections.abc import Iterator, Mapping, MutableMapping, Sequence
from qutebrowser.config import config
from qutebrowser.utils import usertypes, utils, log, version
# Note: We *cannot* initialize QtWebEngine (even implicitly) in here, but checking for
# the enum attribute seems to be okay.
from qutebrowser.qt.webenginecore import QWebEngineSettings
_BLINK_SETTINGS = 'blink-settings'
@ -138,6 +147,7 @@ class Variant(enum.Enum):
qt_515_3 = enum.auto()
qt_64 = enum.auto()
qt_66 = enum.auto()
qt_67 = enum.auto()
# Mapping from a colors.webpage.darkmode.algorithm setting value to
@ -187,11 +197,6 @@ _BOOLS = {
False: 'false',
}
_INT_BOOLS = {
True: '1',
False: '0',
}
@dataclasses.dataclass
class _Setting:
@ -207,7 +212,7 @@ class _Setting:
return str(value)
return str(self.mapping[value])
def chromium_tuple(self, value: Any) -> Optional[Tuple[str, str]]:
def chromium_tuple(self, value: Any) -> Optional[tuple[str, str]]:
"""Get the Chromium key and value, or None if no value should be set."""
if self.mapping is not None and self.mapping[value] is None:
return None
@ -237,7 +242,7 @@ class _Definition:
def __init__(
self,
*args: _Setting,
mandatory: Set[str],
mandatory: set[str],
prefix: str,
switch_names: Mapping[Optional[str], str] = None,
) -> None:
@ -250,7 +255,7 @@ class _Definition:
else:
self._switch_names = {None: _BLINK_SETTINGS}
def prefixed_settings(self) -> Iterator[Tuple[str, _Setting]]:
def prefixed_settings(self) -> Iterator[tuple[str, _Setting]]:
"""Get all "prepared" settings.
Yields tuples which contain the Chromium setting key (e.g. 'blink-settings' or
@ -260,26 +265,25 @@ class _Definition:
switch = self._switch_names.get(setting.option, self._switch_names[None])
yield switch, setting.with_prefix(self.prefix)
def copy_with(self, attr: str, value: Any) -> '_Definition':
"""Get a new _Definition object with a changed attribute.
NOTE: This does *not* copy the settings list. Both objects will reference the
same (immutable) tuple.
"""
new = copy.copy(self)
setattr(new, attr, value)
return new
def copy_add_setting(self, setting: _Setting) -> '_Definition':
"""Get a new _Definition object with an additional setting."""
new = copy.copy(self)
new._settings = self._settings + (setting,) # pylint: disable=protected-access
return new
def copy_remove_setting(self, name: str) -> '_Definition':
"""Get a new _Definition object with a setting removed."""
new = copy.copy(self)
filtered_settings = tuple(s for s in self._settings if s.option != name)
if len(filtered_settings) == len(self._settings):
raise ValueError(f"Setting {name} not found in {self}")
new._settings = filtered_settings # pylint: disable=protected-access
return new
def copy_replace_setting(self, option: str, chromium_key: str) -> '_Definition':
"""Get a new _Definition object with `old` replaced by `new`.
If `old` is not in the settings list, return the old _Definition object.
If `old` is not in the settings list, raise ValueError.
"""
new = copy.deepcopy(self)
@ -332,6 +336,8 @@ _DEFINITIONS[Variant.qt_64] = _DEFINITIONS[Variant.qt_515_3].copy_replace_settin
_DEFINITIONS[Variant.qt_66] = _DEFINITIONS[Variant.qt_64].copy_add_setting(
_Setting('policy.images', 'ImageClassifierPolicy', _IMAGE_CLASSIFIERS),
)
# Qt 6.7: Enabled is now handled dynamically via QWebEngineSettings
_DEFINITIONS[Variant.qt_67] = _DEFINITIONS[Variant.qt_66].copy_remove_setting('enabled')
_SettingValType = Union[str, usertypes.Unset]
@ -367,7 +373,14 @@ def _variant(versions: version.WebEngineVersions) -> Variant:
except KeyError:
log.init.warning(f"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}")
if versions.webengine >= utils.VersionNumber(6, 6):
if (
# We need a PyQt 6.7 as well with the API available, otherwise we can't turn on
# dark mode later in webenginesettings.py.
versions.webengine >= utils.VersionNumber(6, 7) and
hasattr(QWebEngineSettings.WebAttribute, 'ForceDarkMode')
):
return Variant.qt_67
elif versions.webengine >= utils.VersionNumber(6, 6):
return Variant.qt_66
elif versions.webengine >= utils.VersionNumber(6, 4):
return Variant.qt_64
@ -386,7 +399,7 @@ def settings(
*,
versions: version.WebEngineVersions,
special_flags: Sequence[str],
) -> Mapping[str, Sequence[Tuple[str, str]]]:
) -> Mapping[str, Sequence[tuple[str, str]]]:
"""Get necessary blink settings to configure dark mode for QtWebEngine.
Args:
@ -400,12 +413,12 @@ def settings(
variant = _variant(versions)
log.init.debug(f"Darkmode variant: {variant.name}")
result: Mapping[str, List[Tuple[str, str]]] = collections.defaultdict(list)
result: Mapping[str, list[tuple[str, str]]] = collections.defaultdict(list)
blink_settings_flag = f'--{_BLINK_SETTINGS}='
for flag in special_flags:
if flag.startswith(blink_settings_flag):
for pair in flag[len(blink_settings_flag):].split(','):
for pair in flag.removeprefix(blink_settings_flag).split(','):
key, val = pair.split('=', maxsplit=1)
result[_BLINK_SETTINGS].append((key, val))

View File

@ -109,6 +109,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
}
new_types = {
"WebSocket": interceptors.ResourceType.websocket, # added in Qt 6.4
"Json": interceptors.ResourceType.json, # added in Qt 6.8
}
for qt_name, qb_value in new_types.items():
qt_value = getattr(
@ -187,7 +188,9 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
if request.is_blocked:
info.block(True)
for header, value in shared.custom_headers(url=url):
for header, value in shared.custom_headers(
url=url, fallback_accept_language=not is_xhr
):
if header.lower() == b'accept' and is_xhr:
# https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
# says: "If no Accept header has been set using this, an Accept header

View File

@ -33,7 +33,8 @@ import dataclasses
import itertools
import functools
import subprocess
from typing import Any, List, Dict, Optional, Iterator, Type, TYPE_CHECKING
from typing import Any, Optional, TYPE_CHECKING
from collections.abc import Iterator
from qutebrowser.qt import machinery
from qutebrowser.qt.core import (Qt, QObject, QVariant, QMetaType, QByteArray, pyqtSlot,
@ -195,7 +196,7 @@ class NotificationBridgePresenter(QObject):
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
self._active_notifications: Dict[int, 'QWebEngineNotification'] = {}
self._active_notifications: dict[int, 'QWebEngineNotification'] = {}
self._adapter: Optional[AbstractNotificationAdapter] = None
config.instance.changed.connect(self._init_adapter)
@ -232,8 +233,8 @@ class NotificationBridgePresenter(QObject):
def _get_adapter_candidates(
self,
setting: str,
) -> List[Type[AbstractNotificationAdapter]]:
candidates: Dict[str, List[Type[AbstractNotificationAdapter]]] = {
) -> list[type[AbstractNotificationAdapter]]:
candidates: dict[str, list[type[AbstractNotificationAdapter]]] = {
"libnotify": [
DBusNotificationAdapter,
SystrayNotificationAdapter,
@ -285,7 +286,10 @@ class NotificationBridgePresenter(QObject):
if replaces_id is None:
if notification_id in self._active_notifications:
raise Error(f"Got duplicate id {notification_id}")
message.error(f"Got duplicate notification id {notification_id} "
f"from {self._adapter.NAME}")
self._drop_adapter()
return
qt_notification.show()
self._active_notifications[notification_id] = qt_notification
@ -665,7 +669,7 @@ class _ServerCapabilities:
kde_origin_name: bool
@classmethod
def from_list(cls, capabilities: List[str]) -> "_ServerCapabilities":
def from_list(cls, capabilities: list[str]) -> "_ServerCapabilities":
return cls(
actions='actions' in capabilities,
body_markup='body-markup' in capabilities,
@ -951,10 +955,10 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
qtutils.extract_enum_val(QMetaType.Type.QStringList),
)
def _get_hints_arg(self, *, origin_url: QUrl, icon: QImage) -> Dict[str, Any]:
def _get_hints_arg(self, *, origin_url: QUrl, icon: QImage) -> dict[str, Any]:
"""Get the hints argument for present()."""
origin_url_str = origin_url.toDisplayString()
hints: Dict[str, Any] = {
hints: dict[str, Any] = {
# Include the origin in case the user wants to do different things
# with different origin's notifications.
"x-qutebrowser-origin": origin_url_str,
@ -984,7 +988,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
title: str,
body: str,
actions: QDBusArgument,
hints: Dict[str, Any],
hints: dict[str, Any],
timeout: int,
) -> Any:
"""Wrapper around DBus call to use keyword args."""

View File

@ -5,7 +5,8 @@
"""QtWebEngine specific part of the web element API."""
from typing import (
TYPE_CHECKING, Any, Callable, Dict, Iterator, Optional, Set, Tuple, Union)
TYPE_CHECKING, Any, Optional, Union)
from collections.abc import Iterator, Callable
from qutebrowser.qt.core import QRect, QEventLoop
from qutebrowser.qt.widgets import QApplication
@ -24,11 +25,11 @@ class WebEngineElement(webelem.AbstractWebElement):
_tab: "webenginetab.WebEngineTab"
def __init__(self, js_dict: Dict[str, Any],
def __init__(self, js_dict: dict[str, Any],
tab: 'webenginetab.WebEngineTab') -> None:
super().__init__(tab)
# Do some sanity checks on the data we get from JS
js_dict_types: Dict[str, Union[type, Tuple[type, ...]]] = {
js_dict_types: dict[str, Union[type, tuple[type, ...]]] = {
'id': int,
'text': str,
'value': (str, int, float),
@ -105,7 +106,7 @@ class WebEngineElement(webelem.AbstractWebElement):
log.stub()
return QRect()
def classes(self) -> Set[str]:
def classes(self) -> set[str]:
"""Get a list of classes assigned to this element."""
return set(self._js_dict['class_name'].split())

View File

@ -12,7 +12,7 @@ Module attributes:
import os
import operator
import pathlib
from typing import cast, Any, List, Optional, Tuple, Union, TYPE_CHECKING
from typing import cast, Any, Optional, Union, TYPE_CHECKING
from qutebrowser.qt import machinery
from qutebrowser.qt.gui import QFont
@ -148,12 +148,20 @@ class WebEngineSettings(websettings.AbstractSettings):
Attr(QWebEngineSettings.WebAttribute.AutoLoadIconsForPage,
converter=lambda val: val != 'never'),
}
try:
_ATTRIBUTES['content.canvas_reading'] = Attr(
QWebEngineSettings.WebAttribute.ReadingFromCanvasEnabled) # type: ignore[attr-defined,unused-ignore]
except AttributeError:
# Added in QtWebEngine 6.6
pass
if machinery.IS_QT6:
try:
_ATTRIBUTES['content.canvas_reading'] = Attr(
QWebEngineSettings.WebAttribute.ReadingFromCanvasEnabled)
except AttributeError:
# Added in QtWebEngine 6.6
pass
try:
_ATTRIBUTES['colors.webpage.darkmode.enabled'] = Attr(
QWebEngineSettings.WebAttribute.ForceDarkMode)
except AttributeError:
# Added in QtWebEngine 6.7
pass
_FONT_SIZES = {
'fonts.web.size.minimum':
@ -208,6 +216,10 @@ class WebEngineSettings(websettings.AbstractSettings):
QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard: True,
QWebEngineSettings.WebAttribute.JavascriptCanPaste: True,
},
'ask': {
QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard: False,
QWebEngineSettings.WebAttribute.JavascriptCanPaste: False,
},
}
def set_unknown_url_scheme_policy(
@ -273,6 +285,7 @@ class ProfileSetter:
self._set_hardcoded_settings()
self.set_persistent_cookie_policy()
self.set_dictionary_language()
self.disable_persistent_permissions_policy()
def _set_hardcoded_settings(self):
"""Set up settings with a fixed value."""
@ -335,7 +348,23 @@ class ProfileSetter:
log.config.debug("Found dicts: {}".format(filenames))
self._profile.setSpellCheckLanguages(filenames)
self._profile.setSpellCheckEnabled(bool(filenames))
should_enable = bool(filenames)
if self._profile.isSpellCheckEnabled() != should_enable:
# Only setting conditionally as a WORKAROUND for a bogus Qt error message:
# https://bugreports.qt.io/browse/QTBUG-131969
self._profile.setSpellCheckEnabled(should_enable)
def disable_persistent_permissions_policy(self):
"""Disable webengine's permission persistence."""
if machinery.IS_QT6: # for mypy
try:
# New in WebEngine 6.8.0
self._profile.setPersistentPermissionsPolicy(
QWebEngineProfile.PersistentPermissionsPolicy.AskEveryTime
)
except AttributeError:
pass
def _update_settings(option):
@ -349,6 +378,12 @@ def _update_settings(option):
def _init_user_agent_str(ua):
global parsed_user_agent
parsed_user_agent = websettings.UserAgent.parse(ua)
if parsed_user_agent.upstream_browser_version.endswith(".0.0.0"):
# https://codereview.qt-project.org/c/qt/qtwebengine/+/616314
# but we still want the full version available to users if they want it.
qtwe_versions = version.qtwebengine_versions()
assert qtwe_versions.chromium is not None
parsed_user_agent.upstream_browser_version = qtwe_versions.chromium
def init_user_agent():
@ -384,6 +419,25 @@ def _init_profile(profile: QWebEngineProfile) -> None:
_global_settings.init_settings()
def _clear_webengine_permissions_json():
"""Remove QtWebEngine's persistent permissions file, if present.
We have our own permissions feature and don't integrate with their one.
This only needs to be called when you are on Qt6.8 but PyQt<6.8, since if
we have access to the `setPersistentPermissionsPolicy()` we will use that
to disable the Qt feature.
This needs to be called before we call `setPersistentStoragePath()`
because Qt will load the file during that.
"""
permissions_file = pathlib.Path(standarddir.data()) / "webengine" / "permissions.json"
try:
permissions_file.unlink(missing_ok=True)
except OSError as err:
log.init.warning(
f"Error while cleaning up webengine permissions file: {err}"
)
def _init_default_profile():
"""Init the default QWebEngineProfile."""
global default_profile
@ -406,6 +460,7 @@ def _init_default_profile():
f" Early version: {non_ua_version}\n"
f" Real version: {ua_version}")
_clear_webengine_permissions_json()
default_profile.setCachePath(
os.path.join(standarddir.cache(), 'webengine'))
default_profile.setPersistentStoragePath(
@ -438,13 +493,13 @@ def _init_site_specific_quirks():
# default_ua = ("Mozilla/5.0 ({os_info}) "
# "AppleWebKit/{webkit_version} (KHTML, like Gecko) "
# "{qt_key}/{qt_version} "
# "{upstream_browser_key}/{upstream_browser_version} "
# "{upstream_browser_key}/{upstream_browser_version_short} "
# "Safari/{webkit_version}")
no_qtwe_ua = ("Mozilla/5.0 ({os_info}) "
"AppleWebKit/{webkit_version} (KHTML, like Gecko) "
"{upstream_browser_key}/{upstream_browser_version} "
"{upstream_browser_key}/{upstream_browser_version_short} "
"Safari/{webkit_version}")
firefox_ua = "Mozilla/5.0 ({os_info}; rv:90.0) Gecko/20100101 Firefox/90.0"
firefox_ua = "Mozilla/5.0 ({os_info}; rv:136.0) Gecko/20100101 Firefox/136.0"
def maybe_newer_chrome_ua(at_least_version):
"""Return a new UA if our current chrome version isn't at least at_least_version."""
@ -501,7 +556,7 @@ def _init_default_settings():
- Make sure the devtools always get images/JS permissions.
- On Qt 6, make sure files in the data path can load external resources.
"""
devtools_settings: List[Tuple[str, Any]] = [
devtools_settings: list[tuple[str, Any]] = [
('content.javascript.enabled', True),
('content.images', True),
('content.cookies.accept', 'all'),
@ -514,7 +569,7 @@ def _init_default_settings():
hide_userconfig=True)
if machinery.IS_QT6:
userscripts_settings: List[Tuple[str, Any]] = [
userscripts_settings: list[tuple[str, Any]] = [
("content.local_content_can_access_remote_urls", True),
("content.local_content_can_access_file_urls", False),
]

View File

@ -12,7 +12,7 @@ import re
import html as html_utils
from typing import cast, Union, Optional
from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QTimer, QUrl,
from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl,
QObject, QByteArray)
from qutebrowser.qt.network import QAuthenticator
from qutebrowser.qt.webenginecore import QWebEnginePage, QWebEngineScript, QWebEngineHistory
@ -816,7 +816,7 @@ class WebEngineAudio(browsertab.AbstractAudio):
# Implements the intended two-second delay specified at
# https://doc.qt.io/archives/qt-5.14/qwebenginepage.html#recentlyAudibleChanged
delay_ms = 2000
self._silence_timer = QTimer(self)
self._silence_timer = usertypes.Timer(self)
self._silence_timer.setSingleShot(True)
self._silence_timer.setInterval(delay_ms)
@ -886,6 +886,8 @@ class _WebEnginePermissions(QObject):
QWebEnginePage.Feature.MouseLock: 'content.mouse_lock',
QWebEnginePage.Feature.DesktopVideoCapture: 'content.desktop_capture',
QWebEnginePage.Feature.DesktopAudioVideoCapture: 'content.desktop_capture',
# 8 == ClipboardReadWrite, new in 6.8
QWebEnginePage.Feature(8): 'content.javascript.clipboard',
}
_messages = {
@ -897,6 +899,7 @@ class _WebEnginePermissions(QObject):
QWebEnginePage.Feature.MouseLock: 'hide your mouse pointer',
QWebEnginePage.Feature.DesktopVideoCapture: 'capture your desktop',
QWebEnginePage.Feature.DesktopAudioVideoCapture: 'capture your desktop and audio',
QWebEnginePage.Feature(8): 'read and write your clipboard',
}
def __init__(self, tab, parent=None):
@ -1477,9 +1480,9 @@ class WebEngineTab(browsertab.AbstractTab):
log.network.debug("Asking for credentials")
answer = shared.authentication_required(
url, authenticator, abort_on=[self.abort_questions])
if not netrc_success and answer is None:
log.network.debug("Aborting auth")
sip.assign(authenticator, QAuthenticator())
if answer is None:
log.network.debug("Aborting auth")
sip.assign(authenticator, QAuthenticator())
@pyqtSlot()
def _on_load_started(self):
@ -1513,7 +1516,7 @@ class WebEngineTab(browsertab.AbstractTab):
browsertab.TerminationStatus.crashed,
QWebEnginePage.RenderProcessTerminationStatus.KilledTerminationStatus:
browsertab.TerminationStatus.killed,
-1:
QWebEnginePage.RenderProcessTerminationStatus(-1):
browsertab.TerminationStatus.unknown,
}
self.renderer_process_terminated.emit(status_map[status], exitcode)

View File

@ -5,7 +5,8 @@
"""The main browser widget for QtWebEngine."""
import mimetypes
from typing import List, Iterable, Optional
from typing import Optional
from collections.abc import Iterable
from qutebrowser.qt import machinery
from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QUrl
@ -135,8 +136,7 @@ class WebEngineView(QWebEngineView):
def page(self) -> "WebEnginePage":
"""Return the page for this view."""
maybe_page = super().page()
assert maybe_page is not None
assert isinstance(maybe_page, WebEnginePage)
assert isinstance(maybe_page, WebEnginePage), maybe_page
return maybe_page
def settings(self) -> "QWebEngineSettings":
@ -316,7 +316,7 @@ class WebEnginePage(QWebEnginePage):
mode: QWebEnginePage.FileSelectionMode,
old_files: Iterable[Optional[str]],
accepted_mimetypes: Iterable[Optional[str]],
) -> List[str]:
) -> list[str]:
"""Override chooseFiles to (optionally) invoke custom file uploader."""
accepted_mimetypes_filtered = [m for m in accepted_mimetypes if m is not None]
old_files_filtered = [f for f in old_files if f is not None]

View File

@ -4,7 +4,8 @@
"""A wrapper over a list of QSslErrors."""
from typing import Sequence, Optional
from typing import Optional
from collections.abc import Sequence
from qutebrowser.qt.network import QSslError, QNetworkReply

View File

@ -4,7 +4,7 @@
"""Handling of HTTP cookies."""
from typing import Sequence
from collections.abc import Sequence
from qutebrowser.qt.network import QNetworkCookie, QNetworkCookieJar
from qutebrowser.qt.core import pyqtSignal, QDateTime

View File

@ -8,7 +8,6 @@ import email.headerregistry
import email.errors
import dataclasses
import os.path
from typing import Type
from qutebrowser.qt.network import QNetworkRequest
@ -25,7 +24,7 @@ class DefectWrapper:
"""Wrapper around a email.error for comparison."""
error_class: Type[email.errors.MessageDefect]
error_class: type[email.errors.MessageDefect]
line: str
def __eq__(self, other):

View File

@ -19,7 +19,7 @@ import email.mime.multipart
import email.message
import quopri
import dataclasses
from typing import MutableMapping, Set, Tuple, Callable
from collections.abc import MutableMapping, Callable
from qutebrowser.qt.core import QUrl
@ -177,7 +177,7 @@ class MHTMLWriter:
return msg
_PendingDownloadType = Set[Tuple[QUrl, downloads.AbstractDownloadItem]]
_PendingDownloadType = set[tuple[QUrl, downloads.AbstractDownloadItem]]
class _Downloader:

View File

@ -7,7 +7,8 @@
import collections
import html
import dataclasses
from typing import TYPE_CHECKING, Dict, MutableMapping, Optional, Set
from typing import TYPE_CHECKING, Optional
from collections.abc import MutableMapping
from qutebrowser.qt.core import pyqtSlot, pyqtSignal, QUrl, QByteArray
from qutebrowser.qt.network import (QNetworkAccessManager, QNetworkReply, QSslConfiguration,
@ -29,7 +30,7 @@ if TYPE_CHECKING:
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
_proxy_auth_cache: Dict['ProxyId', 'prompt.AuthInfo'] = {}
_proxy_auth_cache: dict['ProxyId', 'prompt.AuthInfo'] = {}
@dataclasses.dataclass(frozen=True)
@ -110,7 +111,7 @@ def init():
_SavedErrorsType = MutableMapping[
urlutils.HostTupleType,
Set[certificateerror.CertificateErrorWrapper],
set[certificateerror.CertificateErrorWrapper],
]

View File

@ -4,7 +4,8 @@
"""Utilities related to QWebHistory."""
from typing import Any, List, Mapping
from typing import Any
from collections.abc import Mapping
from qutebrowser.qt.core import QByteArray, QDataStream, QIODevice, QUrl
@ -66,7 +67,7 @@ def serialize(items):
"""
data = QByteArray()
stream = QDataStream(data, QIODevice.OpenModeFlag.ReadWrite)
user_data: List[Mapping[str, Any]] = []
user_data: list[Mapping[str, Any]] = []
current_idx = None

View File

@ -4,7 +4,8 @@
"""QtWebKit specific part of the web element API."""
from typing import cast, TYPE_CHECKING, Iterator, List, Optional, Set
from typing import cast, TYPE_CHECKING, Optional
from collections.abc import Iterator
from qutebrowser.qt.core import QRect, Qt
# pylint: disable=no-name-in-module
@ -90,7 +91,7 @@ class WebKitElement(webelem.AbstractWebElement):
self._check_vanished()
return self._elem.geometry()
def classes(self) -> Set[str]:
def classes(self) -> set[str]:
self._check_vanished()
return set(self._elem.classes())
@ -364,7 +365,7 @@ class WebKitElement(webelem.AbstractWebElement):
super()._click_fake_event(click_target)
def get_child_frames(startframe: QWebFrame) -> List[QWebFrame]:
def get_child_frames(startframe: QWebFrame) -> list[QWebFrame]:
"""Get all children recursively of a given QWebFrame.
Loosely based on https://blog.nextgenetics.net/?e=64
@ -378,7 +379,7 @@ def get_child_frames(startframe: QWebFrame) -> List[QWebFrame]:
results = []
frames = [startframe]
while frames:
new_frames: List[QWebFrame] = []
new_frames: list[QWebFrame] = []
for frame in frames:
results.append(frame)
new_frames += frame.childFrames()

View File

@ -7,7 +7,8 @@
import re
import functools
import xml.etree.ElementTree
from typing import cast, Iterable, Optional
from typing import cast, Optional
from collections.abc import Iterable
from qutebrowser.qt.core import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize
from qutebrowser.qt.gui import QIcon

View File

@ -14,6 +14,8 @@ For command arguments, there are also some variables you can use:
- `{url:host}`, `{url:domain}`, `{url:auth}`, `{url:scheme}`, `{url:username}`,
`{url:password}`, `{url:port}`, `{url:path}` and `{url:query}`
expand to the respective parts of the current URL
- `{url:yank}` expands to the URL of the current page but strips all the query
parameters in the `url.yank_ignored_parameters` setting.
- `{title}` expands to the current page's title
- `{clipboard}` expands to the clipboard contents
- `{primary}` expands to the primary selection contents

View File

@ -7,7 +7,6 @@
Defined here to avoid circular dependency hell.
"""
from typing import List
import difflib
@ -21,7 +20,7 @@ class NoSuchCommandError(Error):
"""Raised when a command isn't found."""
@classmethod
def for_cmd(cls, cmd: str, all_commands: List[str] = None) -> "NoSuchCommandError":
def for_cmd(cls, cmd: str, all_commands: list[str] = None) -> "NoSuchCommandError":
"""Raise an exception for the given command."""
suffix = ''
if all_commands:

View File

@ -9,8 +9,8 @@ import collections
import traceback
import typing
import dataclasses
from typing import (Any, MutableMapping, MutableSequence, Tuple, Union, List, Optional,
Callable)
from typing import (Any, Union, Optional)
from collections.abc import MutableMapping, MutableSequence, Callable
from qutebrowser.api import cmdutils
from qutebrowser.commands import cmdexc, argparser
@ -30,7 +30,7 @@ class ArgInfo:
metavar: Optional[str] = None
flag: Optional[str] = None
completion: Optional[Callable[..., completionmodel.CompletionModel]] = None
choices: Optional[List[str]] = None
choices: Optional[list[str]] = None
class Command:
@ -107,10 +107,10 @@ class Command:
self.parser.add_argument('-h', '--help', action=argparser.HelpAction,
default=argparser.SUPPRESS, nargs=0,
help=argparser.SUPPRESS)
self.opt_args: MutableMapping[str, Tuple[str, str]] = collections.OrderedDict()
self.opt_args: MutableMapping[str, tuple[str, str]] = collections.OrderedDict()
self.namespace = None
self._count = None
self.pos_args: MutableSequence[Tuple[str, str]] = []
self.pos_args: MutableSequence[tuple[str, str]] = []
self.flags_with_args: MutableSequence[str] = []
self._has_vararg = False

Some files were not shown because too many files have changed in this diff Show More