Compare commits

..

No commits in common. "main" and "v3.4.0" have entirely different histories.
main ... v3.4.0

158 changed files with 876 additions and 3191 deletions

19
.bumpversion.cfg Normal file
View File

@ -0,0 +1,19 @@
[bumpversion]
current_version = 3.4.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})

View File

@ -1,26 +0,0 @@
[tool.bumpversion]
current_version = "3.6.3"
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

@ -4,7 +4,6 @@ include =
tests/*
scripts/*
branch = true
patch = subprocess
omit =
qutebrowser/__main__.py
*/__init__.py

View File

@ -42,7 +42,6 @@ exclude = .*,__pycache__,resources.py
# W503: like break before binary operator
# W504: line break after binary operator
# FI18: __future__ import "annotations" missing
# FI58: __future__ import "annotations" present
# PT004: fixture '{name}' does not return anything, add leading underscore
# PT011: pytest.raises(ValueError) is too broad, set the match parameter or use a more specific exception
# PT012: pytest.raises() block should contain a single simple statement
@ -55,7 +54,7 @@ ignore =
D102,D103,D106,D107,D104,D105,D209,D211,D401,D402,D403,D412,D413,
A003,
W503, W504,
FI18,FI58,
FI18,
PT004,
PT011,
PT012

View File

@ -17,6 +17,8 @@ jobs:
matrix:
include:
- testenv: bleeding
image: "archlinux-webengine-unstable-qt6"
- testenv: bleeding-qt5
image: "archlinux-webengine-unstable"
container:
image: "qutebrowser/ci:${{ matrix.image }}"
@ -31,13 +33,14 @@ jobs:
- /home/runner/work/_temp/:/home/runner/work/_temp/
options: --privileged --tty
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
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
@ -48,7 +51,7 @@ jobs:
shell: bash
if: failure()
- name: Upload screenshots
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}"
path: |

View File

@ -14,7 +14,7 @@ jobs:
linters:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
timeout-minutes: 10
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
@ -27,6 +27,7 @@ jobs:
- testenv: vulture
- testenv: misc
- testenv: pyroma
- testenv: check-manifest
- testenv: eslint
- testenv: shellcheck
args: "-f gcc" # For problem matchers
@ -34,20 +35,20 @@ jobs:
- testenv: actionlint
- testenv: package
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/cache@v5
- uses: actions/cache@v4
with:
path: |
.mypy_cache
.tox
~/.cache/pip
key: "${{ matrix.testenv }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('scripts/dev/pylint_checkers/qute_pylint/*.py') }}"
- uses: actions/setup-python@v6
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version: '22.x'
if: "matrix.testenv == 'eslint'"
@ -85,15 +86,21 @@ jobs:
tests-docker:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
timeout-minutes: 45
runs-on: ubuntu-22.04 # not 24.04 because sandboxing fails by default (#8424)
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
include:
- testenv: py
- testenv: py-qt5
image: archlinux-webkit
- testenv: py-qt5
image: archlinux-webengine
- testenv: py
- testenv: py-qt5
image: archlinux-webengine-unstable
- testenv: py
image: archlinux-webengine-qt6
- testenv: py
image: archlinux-webengine-unstable-qt6
container:
image: "qutebrowser/ci:${{ matrix.image }}"
env:
@ -106,7 +113,7 @@ jobs:
- /home/runner/work/_temp/:/home/runner/work/_temp/
options: --privileged --tty
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up problem matchers
@ -121,7 +128,7 @@ jobs:
shell: bash
if: failure()
- name: Upload screenshots
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}"
path: |
@ -139,7 +146,7 @@ jobs:
include:
### PyQt 5.15.2 (Python 3.9)
- testenv: py39-pyqt5152
os: ubuntu-22.04
os: ubuntu-20.04
python: "3.9"
### PyQt 5.15 (Python 3.10, with coverage)
# FIXME:qt6
@ -152,7 +159,7 @@ jobs:
python: "3.11"
### PyQt 6.2 (Python 3.9)
- testenv: py39-pyqt62
os: ubuntu-22.04
os: ubuntu-20.04
python: "3.9"
### PyQt 6.3 (Python 3.9)
- testenv: py39-pyqt63
@ -186,41 +193,26 @@ jobs:
- testenv: py313-pyqt68
os: ubuntu-24.04
python: "3.13"
### PyQt 6.8 (Python 3.13)
### macOS Ventura
- testenv: py313-pyqt68
os: ubuntu-24.04
os: macos-13
python: "3.13"
### PyQt 6.9 (Python 3.14)
- testenv: py314-pyqt69
os: ubuntu-24.04
python: "3.14"
### PyQt 6.10 (Python 3.14)
- testenv: py314-pyqt610
os: ubuntu-24.04
python: "3.14"
### macOS Sonoma (M1 runner)
- testenv: py314-pyqt610
os: macos-14
python: "3.14"
args: "tests/unit" # Only run unit tests on macOS
### macOS Sequoia (Intel runner)
- testenv: py314-pyqt610
os: macos-15-intel
python: "3.14"
### macOS Sonoma (M1 runner)
- testenv: py313-pyqt68
os: macos-14
python: "3.13"
args: "tests/unit" # Only run unit tests on macOS
### Windows
- testenv: py314-pyqt610
os: windows-2022
python: "3.14"
- testenv: py314-pyqt610
os: windows-2025
python: "3.14"
- testenv: py313-pyqt68
os: windows-2019
python: "3.13"
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/cache@v5
- uses: actions/cache@v4
with:
path: |
.mypy_cache
@ -228,7 +220,7 @@ jobs:
~/.cache/pip
key: "${{ matrix.testenv }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}"
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: "${{ matrix.python }}"
- name: Set up problem matchers
@ -236,7 +228,7 @@ jobs:
- name: Install apt dependencies
run: |
sudo apt-get update
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 libjpeg-dev
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: |
@ -269,7 +261,7 @@ jobs:
shell: bash
if: failure()
- name: Upload screenshots
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.testenv }}-${{ matrix.os }}"
path: |
@ -282,24 +274,24 @@ jobs:
permissions:
security-events: write
timeout-minutes: 15
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v3
with:
languages: javascript, python
queries: +security-extended
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v3
irc:
timeout-minutes: 2
continue-on-error: true
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs: [linters, tests, tests-docker, codeql]
if: "always() && github.repository_owner == 'qutebrowser'"
steps:

View File

@ -13,11 +13,14 @@ jobs:
fail-fast: false
matrix:
image:
- archlinux-webkit
- archlinux-webengine
- archlinux-webengine-unstable
- archlinux-webengine-unstable-qt6
- archlinux-webengine-qt6
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- run: pip install jinja2

View File

@ -14,45 +14,36 @@ jobs:
fail-fast: false
matrix:
include:
- os: macos-15-intel
- os: macos-13
toxenv: build-release
name: macos-intel
- os: macos-14
toxenv: build-release
name: macos-apple-silicon
- os: windows-latest
- os: windows-2019
toxenv: build-release
name: windows
- os: macos-15-intel
- os: macos-13
args: --debug
toxenv: build-release
name: macos-debug-intel
- os: macos-14
toxenv: build-release
name: macos-debug-apple-silicon
- os: windows-latest
- os: windows-2019
args: --debug
toxenv: build-release
name: windows-debug
runs-on: "${{ matrix.os }}"
timeout-minutes: 45
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install nsis
if: "matrix.os == 'windows-latest'"
run: |
irm get.scoop.sh | iex
scoop update
scoop bucket add extras
scoop install nsis
Add-Content $env:GITHUB_PATH "C:\Users\runneradmin\scoop\shims"
shell: pwsh
- name: Install dependencies
run: |
python -m pip install -U pip
@ -72,7 +63,7 @@ jobs:
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
shell: bash
- name: Upload artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: "qutebrowser-nightly-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.name }}"
path: |

View File

@ -18,11 +18,11 @@ jobs:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.9
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Recompile requirements
@ -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@v8
uses: peter-evans/create-pull-request@v7
with:
committer: qutebrowser bot <bot@qutebrowser.org>
author: qutebrowser bot <bot@qutebrowser.org>

View File

@ -12,12 +12,11 @@ on:
- 'patch'
- 'minor'
- 'major'
- 'reupload' # reupload last release
# FIXME do we want a possibility to do prereleases here?
python_version:
description: 'Python version'
required: true
default: '3.14'
default: '3.13'
type: choice
options:
- '3.9'
@ -25,20 +24,18 @@ on:
- '3.11'
- '3.12'
- '3.13'
- '3.14'
jobs:
prepare:
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
version: ${{ steps.bump.outputs.version }}
version_x: ${{ steps.bump.outputs.version_x }}
release_id: ${{ inputs.release_type == 'reupload' && steps.find-release.outputs.result || steps.create-release.outputs.id }}
release_id: ${{ steps.create-release.outputs.id }}
permissions:
contents: write # To push release commit/tag
steps:
- name: Find release branch
uses: actions/github-script@v8
uses: actions/github-script@v7
id: find-branch
with:
script: |
@ -62,9 +59,9 @@ jobs:
console.log(`sorted: ${sorted}`);
return sorted.at(-1);
result-encoding: string
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
# Doesn't really matter what we prepare the release with, but let's
# use the same version for consistency.
@ -78,7 +75,7 @@ jobs:
git config --global user.name "qutebrowser bot"
git config --global user.email "bot@qutebrowser.org"
- name: Switch to release branch
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: ${{ steps.find-branch.outputs.result }}
- name: Import GPG Key
@ -86,9 +83,9 @@ jobs:
gpg --import <<< "${{ secrets.QUTEBROWSER_BOT_GPGKEY }}"
- name: Bump version
id: bump
run: "tox -e update-version -- ${{ inputs.release_type }}"
run: "tox -e update-version -- ${{ github.event.inputs.release_type }}"
- name: Check milestone
uses: actions/github-script@v8
uses: actions/github-script@v7
with:
script: |
const milestones = await github.paginate(github.rest.issues.listMilestones, {
@ -103,60 +100,35 @@ jobs:
core.setFailed(`Found open milestone ${milestone.title} with ${milestone.open_issues} open and ${milestone.closed_issues} closed issues!`);
}
- name: Push release commit/tag
if: ${{ inputs.release_type != 'reupload' }}
run: |
git push origin ${{ steps.find-branch.outputs.result }}
git push origin v${{ steps.bump.outputs.version }}
- name: Cherry-pick release commit
if: ${{ inputs.release_type == 'patch' }}
if: ${{ github.event.inputs.release_type == 'patch' }}
run: |
git fetch origin main
git checkout main
git cherry-pick -x v${{ steps.bump.outputs.version }}
git push origin main
git checkout v${{ steps.bump.outputs.version_x }}
- name: Create release branch
if: ${{ inputs.release_type == 'minor' || inputs.release_type == 'major' }}
if: ${{ github.event.inputs.release_type != 'patch' }}
run: |
git checkout -b v${{ steps.bump.outputs.version_x }}
git push --set-upstream origin v${{ steps.bump.outputs.version_x }}
- name: Create GitHub draft release
if: ${{ inputs.release_type != 'reupload' }}
id: create-release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.bump.outputs.version }}
draft: true
body: "*Release artifacts for this release are currently being uploaded...*"
- name: Find GitHub draft release
if: ${{ inputs.release_type == 'reupload' }}
id: find-release
uses: actions/github-script@v8
with:
script: |
const releases = await github.paginate(github.rest.repos.listReleases, {
owner: context.repo.owner,
repo: context.repo.repo,
});
const names = releases.map(release => release.name);
console.log(`releases: ${names}`);
const release = releases.find(release => release.tag_name === "v${{ steps.bump.outputs.version }}");
if (release === undefined) {
core.setFailed(`No release found with tag v${{ steps.bump.outputs.version }}!`);
}
if (!release.draft) {
core.setFailed(`Release ${release.tag_name} is not a draft release!`);
}
return release.id;
result-encoding: string
release:
strategy:
matrix:
include:
- os: macos-14-large # Intel
- os: macos-14 # Apple Silicon
- os: windows-2022
- os: macos-13
- os: macos-14
- os: windows-2019
- os: ubuntu-24.04
runs-on: "${{ matrix.os }}"
timeout-minutes: 45
@ -164,13 +136,13 @@ jobs:
permissions:
contents: write # To upload release artifacts
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
ref: v${{ inputs.release_type == 'reupload' && needs.prepare.outputs.version_x || needs.prepare.outputs.version }}
ref: v${{ needs.prepare.outputs.version }}
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python_version }}
python-version: ${{ github.event.inputs.python_version }}
- name: Import GPG Key
if: ${{ startsWith(matrix.os, 'ubuntu-') }}
run: |
@ -195,7 +167,7 @@ jobs:
# FIXME consider switching to trusted publishers:
# https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/
- name: Build and upload release
run: "tox -e build-release -- --upload --no-confirm ${{ inputs.release_type == 'reupload' && '--reupload' || '' }}"
run: "tox -e build-release -- --upload --no-confirm"
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.QUTEBROWSER_BOT_PYPI_TOKEN }}
@ -208,7 +180,7 @@ jobs:
contents: write # To change release
steps:
- name: Publish final release
uses: actions/github-script@v8
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.updateRelease({

View File

@ -20,7 +20,6 @@ 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

@ -15,220 +15,6 @@ 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.6.4]]
v3.6.4 (unreleased)
-------------------
Fixed
~~~~~
- datalist dropdowns not opening correctly on Wayland/Sway (#8831).
This was caused by an old workaround for a different QtWebEngine issue,
which is now disabled for QtWebEngine 6.6.3 and newer.
[[v3.6.3]]
v3.6.3 (2025-11-30)
-------------------
Fixed
~~~~~
- New `qt.workarounds.disable_accessibility` setting, which disables Chromium
accessibility support. By default, is it set to `auto`, which only disables
accessibility on Qt versions with known issues. This works around a bug in Qt
6.10.1 causing frequent segfaults (#8797).
[[v3.6.2]]
v3.6.2 (2025-11-27)
-------------------
Changed
~~~~~~~
* Windows and macOS releases now ship with Qt 6.10.1, which include
security patches up to Chromium 142.0.7444.162.
Fixed
~~~~~
- The version info now includes the Wayland compositor name if wayland-client is
available under a different name than `libwayland-client.so` (#8771).
- The list of Chromium extensions in `--version` / `:version` now uses the
correct Chromium data profile, also fixing a crash with Qt 6.10.1 (#8785).
- With Qt 6.10.1, `qt.workarounds.disable_hangouts_extension` now doesn't apply
on private profiles, avoiding a Qt bug leading to a crash (#8785).
[[v3.6.1]]
v3.6.1 (2025-11-03)
-------------------
Fixed
~~~~~
- A regression in v3.6.0 where the page didn't have keyboard focus after closing
the completion, so e.g. typing in an input field after hinting didn't work.
(#8750)
[[v3.6.0]]
v3.6.0 (2025-10-24)
-------------------
Added
~~~~~
- The `:version` info now shows additional information:
* The X11 window manager / Wayland compositor name (mostly useful for
bug/crash reports).
* Loaded WebExtensions (partial support landed in QtWebEngine 6.10, no
official qutebrowser support yet).
- Support for hinting elements which are part of an (open) shadow DOM.
Changed
~~~~~~~
- The `qutedmenu` userscript now sorts history by the last access time.
- Hardware accelerated 2D canvas is now enabled by default on Qt 6.8.2+,
as graphic glitches with e.g. PDF.js and Google Sheets should be fixed
nowadays. If you still run into issues, please report them and set
`qt.workarounds.disable_accelerated_2d_canvas` to `always` to disable it
again.
- Changes to binary releases:
* Windows and macOS releases are now built with Qt 6.10.0, which is based
on Chromium 134.0.6998.208 with security patches up to 140.0.7339.207.
* Windows and macOS releases are now built with Python 3.14.
* Windows releases are now built on Windows Server 2022 (previously 2019),
which might break compatibility with older Windows releases (untested).
* If using `mkvenv.py` on Linux, note that Qt now requires glibc v2.34 (v2.28
previously). This is available down to Ubuntu 22.04 LTS and Debian Bookworm
(oldstable), so this should not affect most users of desktop distributions.
Fixed
~~~~~
- Fixed crash if two new downloads start while a download prompt is already open
(#8674).
- Fixed exception when closing a qutebrowser window while a download prompt is
still open.
- Hopefully proper fix for some web pages jumping to the top when the statusbar
is hidden (#8223).
- Fix for the page header being shown on YouTube after the fullscreen
notification was hidden (#8625).
- Fix for videos losing keyboard focus when the fullscreen notification shows
(#8174).
- The workaround for microphone/camera permissions not being requested with
QtWebEngine 6.9 on Google Meet, Zoom, or other pages using the new
`<permission>` element now got extended to Qt 6.9.1+ as it's still not fixed
upstream. (#8612)
- The package version for Jinja 3.3+ is now correctly displayed in `:version`.
- Fixed crash with Qt 6.10 (and possibly older Qt versions) when navigating
from a `qute://` page to a web page, e.g. when searching on `qute://start`.
- On Wayland with Qt <= 6.9, `EGL_PLATFORM=wayland` is now set by qutebrowser to
get hardware rendering. Qt 6.10 includes an equivalent fix (#8637).
- Added workaround for per-domain User-Agent header not being used on redirects
(#8679).
- Added site-specific quirk for gitlab.gnome.org agressively blocking old
Chromium versions (and thus QtWebEngine) (#8509).
- Using `:config-list-remove` with an invalid value for the respective option
type now corrently displays an error instead of crashing.
[[v3.5.1]]
v3.5.1 (2025-06-05)
-------------------
Deprecated
~~~~~~~~~~
- QtWebKit (legacy) support got removed from CI and is now untested. If it
breaks, it's not going to be fixed, and support will be removed over the next
releases.
- Qt 5 support is currently still tested, but is also planned to get removed
over the next releases. Same goes for support for older Qt 6 versions (likely
6.2/6.3/6.4 and perhaps 6.5, see https://github.com/qutebrowser/qutebrowser/issues/8464[#8464]).
Changed
~~~~~~~
- Windows/macOS releases now bundle Qt 6.9.1, including many graphics-related
bugfixes, as well as security patches up to Chromium 136.0.7103.114.
Fixed
~~~~~
- A bogus "wildcard call disconnects from destroyed signal" warning from Qt is
now suppressed.
- PDF.js now loads correctly on Windows installations with broken mimetype
configurations.
- A "Ignoring new child ..." debug log message which got spammy with Qt 6.9 is
now removed.
- A unknown crash (possibly related to using devtools) due to weird (Py)Qt
behavior now has a workaround.
- No "QtWebEngine version mismatch" warning is now logged anymore with newer Qt
5.15 releases (but you should still stop using Qt 5).
- The PDF.js version can now correctly be extracted/displayed with newer PDF.js
versions.
- The `qute-bitwarden`, `-lastpass` and `-pass` userscripts now properly avoid
a `DeprecationWarning` from the upcoming 6.0 release of `tldextract`. The
previous fix in v3.5.1 was insufficient.
[[v3.5.0]]
v3.5.0 (2025-04-12)
-------------------
Changed
~~~~~~~
- Windows/macOS releases are now built with Qt 6.9.0
* Based on Chromium 130.0.6723.192
* Security fixes up to Chromium 133.0.6943.141
* Also fixes issues with opening links on macOS
- 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.
- The default user agent now uses the shortened Chromium version and doesn't
expose the `QtWebEngine/...` part anymore, thus making it equal to the
corresponding Chromium user agent. This increases compatibilty due to various
overzealous "security" products used by a variety of websites that block
QtWebEngine, presumably as a bot (known issues existed with Whatsapp Web, UPS,
Digitec Galaxus).
- Changed features in userscripts:
* `qute-bitwarden` now passes your password to the subprocess in an
environment variable when unlocking your vault, instead of as a command
line argument. (#7781)
- New `-D no-system-pdfjs` debug flag to ignore system-wide PDF.js installations
for testing.
- Polyfill for missing `URL.parse` with PDF.js v5 and QtWebEngine < 6.9. Note
this is a "best effort" fix and you should be using the "older browsers"
("legacy") build of PDF.js instead.
Removed
~~~~~~~
- The `ua-slack` site-specific quirk, as things seem to work better nowadays
without a quirk needed.
- The `ua-whatsapp` site-specific quirk, as it's unneeded with the default UA
change described above.
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).
- Crashes when running `:tab-move` or `:yank title` at startup, before a tab is
available.
- Crash with `input.insert_mode.auto_load`, when closing a new tab quickly after
opening it, but before it was fully loaded. (#3895, #8400)
- Workaround for microphone/camera permissions not being requested with
QtWebEngine 6.9.0 on Google Meet, Zoom, or other pages using the new
`<permission>` element. (#8539)
- Resolved issues in userscripts:
* `qute-bitwarden` will now prompt a re-login if its cached session has
been invalidated since last used. (#8456)
* `qute-bitwarden`, `-lastpass` and `-pass` now avoid a
`DeprecationWarning` from the upcoming 6.0 release of `tldextract`
[[v3.4.0]]
v3.4.0 (2024-12-14)
-------------------

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/contribute[Issues which should
* https://github.com/qutebrowser/qutebrowser/labels/easy[Issues which should
be easy to solve]
* https://github.com/qutebrowser/qutebrowser/labels/component%3A%20docs[Documentation issues which require little/no coding]
@ -121,6 +121,8 @@ Currently, the following tox environments are available:
* `pyroma`: Check packaging practices with
https://pypi.python.org/pypi/pyroma/[pyroma].
* `eslint`: Run https://eslint.org/[ESLint] javascript checker.
* `check-manifest`: Check MANIFEST.in completeness with
https://github.com/mgedmin/check-manifest[check-manifest].
* `mkvenv`: Bootstrap a virtualenv for testing.
* `misc`: Run `scripts/misc_checks.py` to check for:
- untracked git files
@ -602,7 +604,6 @@ Info pages:
- chrome://device-log/ (QtWebEngine >= 6.3)
- chrome://gpu/
- chrome://sandbox/ (Linux only)
- chrome://qt/ (QtWebEngine >= 6.7)
Misc. / Debugging pages:
@ -613,7 +614,6 @@ Misc. / Debugging pages:
- chrome://ukm/ (QtWebEngine >= 5.15.3)
- chrome://user-actions/ (QtWebEngine >= 5.15.3)
- chrome://webrtc-logs/ (QtWebEngine >= 5.15.3)
- chrome://extensions/ (QtWebEngine >= 6.10)
Internals pages:
@ -804,8 +804,7 @@ qutebrowser release
**Automatic release via GitHub Actions (starting with v3.0.0):**
* Double check Python version in `.github/workflows/release.yml`
* Run the `release` workflow on the `main` branch, e.g. via `gh workflow run release -f release_type=minor` (`release_type` can be `major`, `minor` or `patch`; you can also override `python_version`)
* Consider running `gh run watch` or `gh run view --web` to watch the progress
* Run the `release` workflow on the `main` branch, e.g. via `gh workflow run release -f release_type=major` (`release_type` can be `major`, `minor` or `patch`; you can also override `python_version`)
**Manual release:**

View File

@ -61,7 +61,7 @@ Why Python?::
point, I wasn't comfortable with C++ so that wasn't an alternative.
But isn't Python too slow for a browser?::
https://www.infoworld.com/article/2303031/van-rossum-python-is-not-too-slow-2.html[It's generally less of a problem than one would expect.]
https://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[It's generally less of a problem than one would expect.]
Most of the heavy lifting of qutebrowser is done by Qt and
QtWebKit/QtWebEngine in C++, with the
https://wiki.python.org/moin/GlobalInterpreterLock[GIL] released.
@ -141,7 +141,7 @@ The comma prefix is used to make sure user-defined bindings don't conflict with
the built-in ones.
+
Note that you might need an additional package (e.g.
https://archlinux.org/packages/extra/any/yt-dlp/[yt-dlp] on
https://www.archlinux.org/packages/community/any/youtube-dl/[youtube-dl] on
Archlinux) to play web videos with mpv.
+
There is a very useful script for mpv, which emulates "unique application"

View File

@ -179,7 +179,7 @@ customizable for a given <<patterns,URL patterns>>.
[source,python]
----
config.set('content.images', False, '*://example.com/*')
config.set('content.images', False, '*://example.com/')
----
Alternatively, you can use `with config.pattern(...) as p:` to get a shortcut
@ -187,7 +187,7 @@ similar to `c.` which is scoped to the given domain:
[source,python]
----
with config.pattern('*://example.com/*') as p:
with config.pattern('*://example.com/') as p:
p.content.images = False
----
@ -416,8 +416,6 @@ 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]
- https://github.com/Rehpotsirhc-z/qutebrowser-doom-one[Doom One]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^

View File

@ -26,7 +26,7 @@ Getting help
You can get help in the IRC channel
link:ircs://irc.libera.chat:6697/#qutebrowser[`#qutebrowser`] on
https://libera.chat/[Libera Chat]
(https://web.libera.chat/#qutebrowser[webchat]),
(https://web.libera.chat/#qutebrowser[webchat], https://matrix.to/#qutebrowser:libera.chat[via Matrix]),
or by writing a message to the
https://listi.jpberlin.de/mailman/listinfo/qutebrowser[mailinglist] at
mailto:qutebrowser@lists.qutebrowser.org[].

View File

@ -302,7 +302,6 @@
|<<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_accessibility,qt.workarounds.disable_accessibility>>|Disable accessibility to avoid crashes on Qt 6.10.1.
|<<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.
@ -2276,22 +2275,21 @@ 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 default user agent of
QtWebKit/QtWebEngine, but with the `QtWebEngine/...` part removed for
increased compatibility.
The default value is equal to the unchanged user agent of
QtWebKit/QtWebEngine.
Note that the value read from JavaScript is always the global value.
Note that the value read from JavaScript is always the global value. With
QtWebEngine between 5.12 and 5.14 (inclusive), changing the value exposed
to JavaScript requires a restart.
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) {upstream_browser_key}/{upstream_browser_version_short} 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} Safari/{webkit_version}]+
[[content.hyperlink_auditing]]
=== content.hyperlink_auditing
@ -2769,9 +2767,10 @@ Type: <<types,FlagList>>
Valid values:
* +ua-whatsapp+
* +ua-google+
* +ua-slack+
* +ua-googledocs+
* +ua-gnome-gitlab+
* +js-whatsapp-web+
* +js-discord+
* +js-string-replaceall+
@ -3992,29 +3991,11 @@ Type: <<types,String>>
Valid values:
* +always+: Disable accelerated 2d canvas
* +auto+: Disable on Qt versions with known issues, enable otherwise
* +auto+: Disable on Qt6 < 6.6.0, enable otherwise
* +never+: Enable accelerated 2d canvas
Default: +pass:[auto]+
[[qt.workarounds.disable_accessibility]]
=== qt.workarounds.disable_accessibility
Disable accessibility to avoid crashes on Qt 6.10.1.
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
Valid values:
* +always+: Disable renderer accessibility
* +auto+: Disable on Qt versions with known issues, enable otherwise
* +never+: Enable renderer accessibility
Default: +pass:[auto]+
[[qt.workarounds.disable_hangouts_extension]]
=== qt.workarounds.disable_hangouts_extension
Disable the Hangouts extension.

View File

@ -103,18 +103,12 @@ 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
------------

View File

@ -44,12 +44,6 @@
</content_rating>
<releases>
<!-- Add new releases here -->
<release version='3.6.3' date='2025-11-30'/>
<release version='3.6.2' date='2025-11-27'/>
<release version='3.6.1' date='2025-11-03'/>
<release version='3.6.0' date='2025-10-24'/>
<release version='3.5.1' date='2025-06-05'/>
<release version='3.5.0' date='2025-04-12'/>
<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"/>

View File

@ -0,0 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
build==1.2.2.post1
check-manifest==0.50
importlib_metadata==8.5.0
packaging==24.2
pyproject_hooks==1.2.0
tomli==2.2.1
zipp==3.21.0

View File

@ -0,0 +1 @@
check-manifest

View File

@ -1,73 +1,54 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
annotated-types==0.7.0
anyio==4.12.0
autocommand==2.2.2
backports.tarfile==1.2.0
bracex==2.6
build==1.3.0
bump-my-version==1.2.5
certifi==2025.11.12
cffi==2.0.0
charset-normalizer==3.4.4
click==8.1.8
cryptography==46.0.3
docutils==0.22.3
exceptiongroup==1.3.1
build==1.2.2.post1
bump2version==1.0.1
certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.4.0
cryptography==44.0.0
docutils==0.21.2
github3.py==4.0.1
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
hunter==3.9.0
id==1.5.0
idna==3.11
importlib_metadata==8.7.0
importlib_resources==6.5.2
hunter==3.7.0
idna==3.10
importlib_metadata==8.5.0
importlib_resources==6.4.5
inflect==7.3.1
jaraco.classes==3.4.0
jaraco.collections==5.1.0
jaraco.context==6.0.1
jaraco.functools==4.0.1
jaraco.functools==4.1.0
jaraco.text==3.12.1
jeepney==0.9.0
keyring==25.7.0
jeepney==0.8.0
keyring==25.5.0
manhole==1.8.1
markdown-it-py==3.0.0
mdurl==0.1.2
more-itertools==10.8.0
nh3==0.3.2
packaging==25.0
platformdirs==4.4.0
prompt_toolkit==3.0.52
pycparser==2.23
pydantic==2.12.5
pydantic-settings==2.11.0
pydantic_core==2.41.5
Pygments==2.19.2
more-itertools==10.5.0
nh3==0.2.19
packaging==24.2
pkginfo==1.12.0
platformdirs==4.3.6
pycparser==2.22
Pygments==2.18.0
PyJWT==2.10.1
Pympler==1.1
pyproject_hooks==1.2.0
PyQt-builder==1.19.1
PyQt-builder==1.17.0
python-dateutil==2.9.0.post0
python-dotenv==1.2.1
questionary==2.1.1
readme_renderer==44.0
requests==2.32.5
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.2.0
rich-click==1.9.4
rich==13.9.4
SecretStorage==3.3.3
sip==6.14.0
sip==6.9.1
six==1.17.0
tomli==2.3.0
tomlkit==0.13.3
twine==6.2.0
tomli==2.2.1
twine==6.0.1
typeguard==4.3.0
typing-inspection==0.4.2
typing_extensions==4.15.0
uritemplate==4.2.0
# urllib3==2.6.2
wcmatch==10.1
wcwidth==0.2.14
zipp==3.23.0
typing_extensions==4.12.2
uritemplate==4.1.1
# urllib3==2.2.3
zipp==3.21.0

View File

@ -1,7 +1,7 @@
hunter
pympler
github3.py
bump-my-version
bump2version
requests
pyqt-builder
build

View File

@ -1,23 +1,23 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==25.4.0
flake8==7.3.0
attrs==24.2.0
flake8==7.1.1
flake8-bugbear==24.12.12
flake8-builtins==3.0.0
flake8-comprehensions==3.17.0
flake8-builtins==2.5.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.1.0
flake8-pytest-style==2.0.0
flake8-string-format==0.3.0
flake8-tidy-imports==4.12.0
flake8-tidy-imports==4.11.0
flake8-tuple==0.4.1
mccabe==0.7.0
pep8-naming==0.15.1
pycodestyle==2.14.0
pep8-naming==0.14.1
pycodestyle==2.12.1
pydocstyle==6.3.0
pyflakes==3.4.0
pyflakes==3.2.0
six==1.17.0
snowballstemmer==3.0.1
snowballstemmer==2.2.0

View File

@ -1,20 +1,19 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
chardet==5.2.0
diff_cover==10.0.0
Jinja2==3.1.6
librt==0.7.3
lxml==6.0.2
MarkupSafe==3.0.3
mypy==1.19.0
mypy_extensions==1.1.0
pathspec==0.12.1
pluggy==1.6.0
Pygments==2.19.2
diff_cover==9.2.0
Jinja2==3.1.4
lxml==5.3.0
MarkupSafe==3.0.2
mypy==1.13.0
mypy-extensions==1.0.0
pluggy==1.5.0
Pygments==2.18.0
PyQt5-stubs==5.15.6.0
tomli==2.3.0
types-colorama==0.4.15.20250801
types-docutils==0.22.3.20251115
types-Pygments==2.19.0.20251121
types-PyYAML==6.0.12.20250915
typing_extensions==4.15.0
tomli==2.2.1
types-colorama==0.4.15.20240311
types-docutils==0.21.0.20241128
types-Pygments==2.18.0.20240506
types-PyYAML==6.0.12.20240917
types-setuptools==75.6.0.20241126
typing_extensions==4.12.2

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
altgraph==0.17.5
importlib_metadata==8.7.0
packaging==25.0
pyinstaller==6.17.0
pyinstaller-hooks-contrib==2025.10
zipp==3.23.0
altgraph==0.17.4
importlib_metadata==8.5.0
packaging==24.2
pyinstaller==6.11.1
pyinstaller-hooks-contrib==2024.10
zipp==3.21.0

View File

@ -1,28 +1,26 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==3.3.11
certifi==2025.11.12
cffi==2.0.0
charset-normalizer==3.4.4
cryptography==46.0.3
dill==0.4.0
astroid==3.3.6
certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.4.0
cryptography==44.0.0
dill==0.3.9
github3.py==4.0.1
idna==3.11
importlib_metadata==8.7.0
isort==6.1.0
idna==3.10
isort==5.13.2
mccabe==0.7.0
pefile==2024.8.26
platformdirs==4.4.0
pycparser==2.23
platformdirs==4.3.6
pycparser==2.22
PyJWT==2.10.1
pylint==3.3.9
pylint==3.3.2
python-dateutil==2.9.0.post0
./scripts/dev/pylint_checkers
requests==2.32.5
requests==2.32.3
six==1.17.0
tomli==2.3.0
tomlkit==0.13.3
typing_extensions==4.15.0
uritemplate==4.2.0
# urllib3==2.6.2
zipp==3.23.0
tomli==2.2.1
tomlkit==0.13.2
typing_extensions==4.12.2
uritemplate==4.1.1
# urllib3==2.2.3

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.17.1
PyQt5_sip==12.16.1
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.11 # rq.filter: < 5.16
PyQt5-Qt5==5.15.18
PyQt5_sip==12.17.1
PyQt5-Qt5==5.15.15
PyQt5_sip==12.16.1
PyQtWebEngine==5.15.7 # rq.filter: < 5.16
PyQtWebEngine-Qt5==5.15.18
PyQtWebEngine-Qt5==5.15.15

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.11
PyQt5-Qt5==5.15.18
PyQt5_sip==12.17.1
PyQt5-Qt5==5.15.15
PyQt5_sip==12.16.1
PyQtWebEngine==5.15.7
PyQtWebEngine-Qt5==5.15.18
PyQtWebEngine-Qt5==5.15.15

View File

@ -1,8 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.10.1
PyQt6-Qt6==6.10.1
PyQt6-WebEngine==6.10.0
PyQt6-WebEngine-Qt6==6.10.1
PyQt6_sip==13.10.2
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/

View File

@ -1,8 +0,0 @@
PyQt6 >= 6.10, < 6.11
PyQt6-Qt6 >= 6.10, < 6.11
PyQt6-WebEngine >= 6.10, < 6.11
PyQt6-WebEngine-Qt6 >= 6.10, < 6.11
# WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2025-October/046347.html
#@ add: --extra-index-url https://www.riverbankcomputing.com/pypi/simple/
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/

View File

@ -4,4 +4,4 @@ PyQt6==6.2.3
PyQt6-Qt6==6.2.4
PyQt6-WebEngine==6.2.1
PyQt6-WebEngine-Qt6==6.2.4
PyQt6_sip==13.10.2
PyQt6_sip==13.9.1

View File

@ -4,4 +4,4 @@ PyQt6==6.3.1
PyQt6-Qt6==6.3.2
PyQt6-WebEngine==6.3.1
PyQt6-WebEngine-Qt6==6.3.2
PyQt6_sip==13.10.2
PyQt6_sip==13.9.1

View File

@ -4,4 +4,4 @@ PyQt6==6.4.2
PyQt6-Qt6==6.4.3
PyQt6-WebEngine==6.4.0
PyQt6-WebEngine-Qt6==6.4.3
PyQt6_sip==13.10.2
PyQt6_sip==13.9.1

View File

@ -4,4 +4,4 @@ PyQt6==6.5.3
PyQt6-Qt6==6.5.3
PyQt6-WebEngine==6.5.0
PyQt6-WebEngine-Qt6==6.5.3
PyQt6_sip==13.10.2
PyQt6_sip==13.9.1

View File

@ -4,4 +4,4 @@ PyQt6==6.6.1
PyQt6-Qt6==6.6.3
PyQt6-WebEngine==6.6.0
PyQt6-WebEngine-Qt6==6.6.3
PyQt6_sip==13.10.2
PyQt6_sip==13.9.1

View File

@ -5,4 +5,4 @@ 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.2
PyQt6_sip==13.9.1

View File

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

View File

@ -1,7 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.9.1
PyQt6-Qt6==6.9.2
PyQt6-WebEngine==6.9.0
PyQt6-WebEngine-Qt6==6.9.2
PyQt6_sip==13.10.2

View File

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

View File

@ -1,8 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.10.1
PyQt6-Qt6==6.10.1
PyQt6-WebEngine==6.10.0
PyQt6-WebEngine-Qt6==6.10.1
PyQt6_sip==13.10.2
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/
PyQt6==6.8.0
PyQt6-Qt6==6.8.1
PyQt6-WebEngine==6.8.0
PyQt6-WebEngine-Qt6==6.8.1
PyQt6_sip==13.9.1

View File

@ -2,7 +2,3 @@ PyQt6
PyQt6-Qt6
PyQt6-WebEngine
PyQt6-WebEngine-Qt6
# WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2025-October/046347.html
#@ add: --extra-index-url https://www.riverbankcomputing.com/pypi/simple/
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/

View File

@ -1,8 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.10.1
PyQt6-Qt6==6.10.1
PyQt6-WebEngine==6.10.0
PyQt6-WebEngine-Qt6==6.10.1
PyQt6_sip==13.10.2
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/
PyQt6==6.8.0
PyQt6-Qt6==6.8.1
PyQt6-WebEngine==6.8.0
PyQt6-WebEngine-Qt6==6.8.1
PyQt6_sip==13.9.1

View File

@ -2,7 +2,3 @@ PyQt6
PyQt6-Qt6
PyQt6-WebEngine
PyQt6-WebEngine-Qt6
# WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2025-October/046347.html
#@ add: --extra-index-url https://www.riverbankcomputing.com/pypi/simple/
--extra-index-url https://www.riverbankcomputing.com/pypi/simple/

View File

@ -1,18 +1,17 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
build==1.3.0
certifi==2025.11.12
charset-normalizer==3.4.4
check-manifest==0.51
docutils==0.22.3
idna==3.11
importlib_metadata==8.7.0
packaging==25.0
Pygments==2.19.2
build==1.2.2.post1
certifi==2024.8.30
charset-normalizer==3.4.0
docutils==0.21.2
idna==3.10
importlib_metadata==8.5.0
packaging==24.2
Pygments==2.18.0
pyproject_hooks==1.2.0
pyroma==5.0.1
requests==2.32.5
tomli==2.3.0
trove-classifiers==2025.12.1.14
urllib3==2.6.2
zipp==3.23.0
pyroma==4.2
requests==2.32.3
tomli==2.2.1
trove-classifiers==2024.10.21.16
urllib3==2.2.3
zipp==3.21.0

View File

@ -1,2 +1 @@
pyroma
check-manifest

View File

@ -1,19 +1,19 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
alabaster==0.7.16
babel==2.17.0
certifi==2025.11.12
charset-normalizer==3.4.4
babel==2.16.0
certifi==2024.8.30
charset-normalizer==3.4.0
docutils==0.21.2
idna==3.11
idna==3.10
imagesize==1.4.1
importlib_metadata==8.7.0
Jinja2==3.1.6
MarkupSafe==3.0.3
packaging==25.0
Pygments==2.19.2
requests==2.32.5
snowballstemmer==3.0.1
importlib_metadata==8.5.0
Jinja2==3.1.4
MarkupSafe==3.0.2
packaging==24.2
Pygments==2.18.0
requests==2.32.3
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
@ -21,6 +21,6 @@ sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
tomli==2.3.0
urllib3==2.6.2
zipp==3.23.0
tomli==2.2.1
urllib3==2.2.3
zipp==3.21.0

View File

@ -2,13 +2,12 @@
# bzr+lp:beautifulsoup
beautifulsoup4
git+https://github.com/cherrypy/cheroot.git
coverage[toml] @ git+https://github.com/nedbat/coveragepy.git
git+https://github.com/nedbat/coveragepy.git#egg=coverage[toml]
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

View File

@ -1,67 +1,67 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==25.4.0
attrs==24.2.0
autocommand==2.2.2
backports.tarfile==1.2.0
beautifulsoup4==4.14.3
beautifulsoup4==4.12.3
blinker==1.9.0
certifi==2025.11.12
charset-normalizer==3.4.4
cheroot==11.1.2
click==8.1.8
coverage==7.10.7
exceptiongroup==1.3.1
execnet==2.1.2
filelock==3.19.1
Flask==3.1.2
certifi==2024.8.30
charset-normalizer==3.4.0
cheroot==10.0.1
click==8.1.7
coverage==7.6.9
exceptiongroup==1.2.2
execnet==2.1.1
filelock==3.16.1
Flask==3.1.0
gherkin-official==29.0.0
hunter==3.9.0
hypothesis==6.141.1
idna==3.11
importlib_metadata==8.7.0
importlib_resources==6.5.2
hunter==3.7.0
hypothesis==6.122.3
idna==3.10
importlib_metadata==8.5.0
importlib_resources==6.4.5
inflect==7.3.1
iniconfig==2.1.0
iniconfig==2.0.0
itsdangerous==2.2.0
jaraco.collections==5.1.0
jaraco.context==6.0.1
jaraco.functools==4.0.1
jaraco.functools==4.1.0
jaraco.text==3.12.1
# Jinja2==3.1.6
Mako==1.3.10
# Jinja2==3.1.4
Mako==1.3.8
manhole==1.8.1
# MarkupSafe==3.0.3
more-itertools==10.8.0
packaging==25.0
# MarkupSafe==3.0.2
more-itertools==10.5.0
packaging==24.2
parse==1.20.2
parse_type==0.6.6
pillow==11.3.0
platformdirs==4.4.0
pluggy==1.6.0
parse_type==0.6.4
pillow==11.0.0
platformdirs==4.3.6
pluggy==1.5.0
py-cpuinfo==9.0.0
Pygments==2.19.2
pytest==8.4.2
Pygments==2.18.0
pytest==8.3.4
pytest-bdd==8.1.0
pytest-benchmark==5.2.3
pytest-cov==7.0.0
pytest-benchmark==5.1.0
pytest-cov==6.0.0
pytest-instafail==0.5.0
pytest-mock==3.15.1
pytest-qt==4.5.0
pytest-repeat==0.9.4
pytest-rerunfailures==16.0.1
pytest-xdist==3.8.0
pytest-xvfb==3.1.1
pytest-mock==3.14.0
pytest-qt==4.4.0
pytest-repeat==0.9.3
pytest-rerunfailures==15.0
pytest-xdist==3.6.1
pytest-xvfb==3.0.0
PyVirtualDisplay==3.0
requests==2.32.5
requests-file==3.0.1
requests==2.32.3
requests-file==2.1.0
six==1.17.0
sortedcontainers==2.4.0
soupsieve==2.8
tldextract==5.3.0
tomli==2.3.0
soupsieve==2.6
tldextract==5.1.3
tomli==2.2.1
typeguard==4.3.0
typing_extensions==4.15.0
urllib3==2.6.2
typing_extensions==4.12.2
urllib3==2.2.3
vulture==2.14
Werkzeug==3.1.4
zipp==3.23.0
Werkzeug==3.1.3
zipp==3.21.0

View File

@ -1,19 +1,18 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
cachetools==6.2.3
cachetools==5.5.0
chardet==5.2.0
colorama==0.4.6
distlib==0.4.0
filelock==3.19.1
packaging==25.0
pip==25.3
platformdirs==4.4.0
pluggy==1.6.0
pyproject-api==1.9.1
setuptools==80.9.0
tomli==2.3.0
tox==4.30.3 ; python_full_version!="3.14.0b1"
typing_extensions==4.15.0
virtualenv==20.35.4
distlib==0.3.9
filelock==3.16.1
packaging==24.2
pip==24.3.1
platformdirs==4.3.6
pluggy==1.5.0
pyproject-api==1.8.0
setuptools==75.6.0
tomli==2.2.1
tox==4.23.2
typing_extensions==4.12.2
virtualenv==20.28.0
wheel==0.45.1
tox @ git+https://github.com/tox-dev/tox ; python_full_version=="3.14.0b1"

View File

@ -1,5 +1,2 @@
tox
wheel
#@ markers: tox python_full_version!="3.14.0b1"
#@ add: tox @ git+https://github.com/tox-dev/tox ; python_full_version=="3.14.0b1"

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
tomli==2.3.0
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.3
yamllint==1.37.1
PyYAML==6.0.2
yamllint==1.35.1

View File

@ -106,8 +106,6 @@ 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

@ -143,7 +143,7 @@ no_entries_found() {
# expected to write the username of that entry to the $username variable and
# the corresponding password to $password
# shellcheck disable=SC2329
# shellcheck disable=SC2317
reset_backend() {
init() { true ; }
query_entries() { true ; }
@ -199,8 +199,7 @@ choose_entry_zenity() {
}
choose_entry_zenity_radio() {
# shellcheck disable=SC2329
zenity_helper() {
zenity_helper() { # shellcheck disable=SC2317
awk '{ print $0 ; print $0 }' \
| zenity --list --radiolist \
--title "qutebrowser password fill" \
@ -280,7 +279,7 @@ pass_backend() {
# =======================================================
# backend: secret
# shellcheck disable=SC2329
# shellcheck disable=SC2317
secret_backend() {
init() {
return

View File

@ -96,8 +96,7 @@ def ask_password(password_prompt_invocation):
raise Exception('Could not unlock vault')
master_pass = process.stdout.strip()
return subprocess.check_output(
['bw', 'unlock', '--raw', '--passwordenv', 'BW_MASTERPASS'],
env={**os.environ, 'BW_MASTERPASS': master_pass},
['bw', 'unlock', '--raw', master_pass],
text=True,
).strip()
@ -133,7 +132,7 @@ def get_session_key(auto_lock, password_prompt_invocation):
def pass_(domain, encoding, auto_lock, password_prompt_invocation):
session_key = get_session_key(auto_lock, password_prompt_invocation)
process = subprocess.run(
['bw', 'list', 'items', '--nointeraction', '--session', session_key, '--url', domain],
['bw', 'list', 'items', '--session', session_key, '--url', domain],
capture_output=True,
)
@ -142,10 +141,6 @@ def pass_(domain, encoding, auto_lock, password_prompt_invocation):
msg = 'Bitwarden CLI returned for {:s} - {:s}'.format(domain, err)
stderr(msg)
if "Vault is locked" in err:
stderr("Bitwarden Vault got locked, trying again with clean session")
return pass_(domain, encoding, 0, password_prompt_invocation)
if process.returncode:
return '[]'
@ -157,7 +152,7 @@ def pass_(domain, encoding, auto_lock, password_prompt_invocation):
def get_totp_code(selection_id, domain_name, encoding, auto_lock, password_prompt_invocation):
session_key = get_session_key(auto_lock, password_prompt_invocation)
process = subprocess.run(
['bw', 'get', 'totp', '--nointeraction', '--session', session_key, selection_id],
['bw', 'get', 'totp', '--session', session_key, selection_id],
capture_output=True,
)
@ -167,10 +162,6 @@ def get_totp_code(selection_id, domain_name, encoding, auto_lock, password_promp
msg = 'Bitwarden CLI returned for {:s} - {:s}'.format(domain_name, err)
stderr(msg)
if "Vault is locked" in err:
stderr("Bitwarden Vault got locked, trying again with clean session")
return get_totp_code(selection_id, domain_name, encoding, 0, password_prompt_invocation)
if process.returncode:
return '[]'
@ -204,20 +195,12 @@ def main(arguments):
# the registered domain name and finally: the IPv4 address if that's what
# the URL represents
candidates = []
for target in filter(
None,
[
extract_result.fqdn,
(
extract_result.top_domain_under_public_suffix
if hasattr(extract_result, "top_domain_under_public_suffix")
else extract_result.registered_domain
),
extract_result.subdomain + "." + extract_result.domain,
extract_result.domain,
extract_result.ipv4,
],
):
for target in filter(None, [
extract_result.fqdn,
extract_result.registered_domain,
extract_result.subdomain + '.' + extract_result.domain,
extract_result.domain,
extract_result.ipv4]):
target_candidates = json.loads(
pass_(
target,

View File

@ -117,20 +117,7 @@ def main(arguments):
# the URL represents
candidates = []
seen_id = set()
for target in filter(
None,
[
extract_result.fqdn,
(
extract_result.top_domain_under_public_suffix
if hasattr(extract_result, "top_domain_under_public_suffix")
else extract_result.registered_domain
),
extract_result.subdomain + extract_result.domain,
extract_result.domain,
extract_result.ipv4,
],
):
for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.subdomain + extract_result.domain, extract_result.domain, extract_result.ipv4]):
target_candidates, err = pass_(target, arguments.io_encoding)
if err:
stderr("LastPass CLI returned for {:s} - {:s}".format(target, err))

View File

@ -243,20 +243,7 @@ def main(arguments):
netloc = urlparse(arguments.url).netloc
for target in filter(
None,
[
extract_result.fqdn,
(
extract_result.top_domain_under_public_suffix
if hasattr(extract_result, "top_domain_under_public_suffix")
else extract_result.registered_domain
),
extract_result.ipv4,
private_domain,
netloc,
],
):
for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.ipv4, private_domain, netloc]):
attempted_targets.append(target)
target_candidates = find_pass_candidates(target, unfiltered=arguments.unfiltered)
if not target_candidates:

View File

@ -23,7 +23,7 @@ create_menu() {
done < "$QUTE_CONFIG_DIR"/bookmarks/urls
# Finally history
printf -- '%s\n' "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory ORDER BY last_atime DESC')"
printf -- '%s\n' "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')"
}
get_selection() {

View File

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

View File

@ -1,12 +0,0 @@
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.check-manifest]
ignore = [
"qutebrowser/git-commit-id",
"qutebrowser/html/doc",
"qutebrowser/html/doc/*",
"qutebrowser/html/doc/img/cheatsheet-*.png",
"*/__pycache__",
]

View File

@ -1,5 +1,4 @@
[pytest]
pythonpath = .
log_level = NOTSET
addopts = --strict-markers --strict-config --instafail --benchmark-columns=Min,Max,Median
testpaths = tests
@ -20,7 +19,6 @@ 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
@ -43,8 +41,6 @@ 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
qt69_ci_skip: Tests which should be skipped with Qt 6.9+ on CI
qt_log_level_fail = WARNING
qt_log_ignore =
# GitHub Actions
@ -82,19 +78,6 @@ qt_log_ignore =
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\.$
# Qt 5 on CI with WebKit
^qglx_findConfig: Failed to finding matching FBConfig for QSurfaceFormat\(version 2\.0, options QFlags<QSurfaceFormat::FormatOption>\(\), depthBufferSize -1, redBufferSize 1, greenBufferSize 1, blueBufferSize 1, alphaBufferSize -1, stencilBufferSize -1, samples -1, swapBehavior QSurfaceFormat::SingleBuffer, swapInterval 1, colorSpace QSurfaceFormat::DefaultColorSpace, profile QSurfaceFormat::NoProfile\)$
# Qt 6.8+ debug build
# https://github.com/qutebrowser/qutebrowser/issues/8069#issuecomment-2017644465
^QObject::connect: Connecting from COMPAT signal \(QWebEnginePage::featurePermissionRequest(ed|Canceled)\(QUrl,QWebEnginePage::Feature\)\)
xfail_strict = true
filterwarnings =
error
@ -102,7 +85,5 @@ filterwarnings =
# 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
# https://github.com/ionelmc/pytest-benchmark/issues/283
ignore:FileType is deprecated\. Simply open files after parsing arguments\.:PendingDeprecationWarning:pytest_benchmark.plugin
faulthandler_timeout = 90
xvfb_colordepth = 24

View File

@ -11,10 +11,10 @@ _year = datetime.date.today().year
__author__ = "Florian Bruhin"
__copyright__ = "Copyright 2013-{} Florian Bruhin (The Compiler)".format(_year)
__license__ = "GPL-3.0-or-later"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version__ = "3.6.3"
__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

@ -29,7 +29,7 @@ if TYPE_CHECKING:
from qutebrowser.keyinput import modeman
from qutebrowser.config import config, websettings
from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils,
urlutils, message, jinja)
urlutils, message, jinja, version)
from qutebrowser.misc import miscwidgets, objects, sessions
from qutebrowser.browser import eventfilter, inspector
from qutebrowser.qt import sip
@ -1177,6 +1177,37 @@ class AbstractTab(QWidget):
navigation.url.errorString()))
navigation.accepted = False
# WORKAROUND for QtWebEngine >= 6.2 not allowing form requests from
# qute:// to outside domains.
needs_load_workarounds = (
objects.backend == usertypes.Backend.QtWebEngine and
version.qtwebengine_versions().webengine >= utils.VersionNumber(6, 2)
)
if (
needs_load_workarounds and
self.url() == QUrl("qute://start/") and
navigation.navigation_type == navigation.Type.form_submitted and
navigation.url.matches(
QUrl(config.val.url.searchengines['DEFAULT']),
urlutils.FormatOption.REMOVE_QUERY)
):
log.webview.debug(
"Working around qute://start loading issue for "
f"{navigation.url.toDisplayString()}")
navigation.accepted = False
self.load_url(navigation.url)
if (
needs_load_workarounds and
self.url() == QUrl("qute://bookmarks/") and
navigation.navigation_type == navigation.Type.back_forward
):
log.webview.debug(
"Working around qute://bookmarks loading issue for "
f"{navigation.url.toDisplayString()}")
navigation.accepted = False
self.load_url(navigation.url)
@pyqtSlot(bool)
def _on_load_finished(self, ok: bool) -> None:
assert self._widget is not None

View File

@ -71,10 +71,7 @@ class CommandDispatcher:
def _current_index(self):
"""Convenience method to get the current widget index."""
current_index = self._tabbed_browser.widget.currentIndex()
if current_index == -1:
raise cmdutils.CommandError("No WebView available yet!")
return current_index
return self._tabbed_browser.widget.currentIndex()
def _current_url(self):
"""Convenience method to get the current url."""
@ -868,6 +865,10 @@ class CommandDispatcher:
Args:
count: How many tabs to switch back.
"""
if self._count() == 0:
# Running :tab-prev after last tab was closed
# See https://github.com/qutebrowser/qutebrowser/issues/1448
return
newidx = self._current_index() - count
if newidx >= 0:
self._set_current_index(newidx)
@ -884,6 +885,10 @@ class CommandDispatcher:
Args:
count: How many tabs to switch forward.
"""
if self._count() == 0:
# Running :tab-next after last tab was closed
# See https://github.com/qutebrowser/qutebrowser/issues/1448
return
newidx = self._current_index() + count
if newidx < self._count():
self._set_current_index(newidx)
@ -1163,7 +1168,7 @@ class CommandDispatcher:
if count is not None:
env['QUTE_COUNT'] = str(count)
idx = self._tabbed_browser.widget.currentIndex()
idx = self._current_index()
if idx != -1:
env['QUTE_TAB_INDEX'] = str(idx + 1)
env['QUTE_TITLE'] = self._tabbed_browser.widget.page_title(idx)

View File

@ -6,12 +6,11 @@
from qutebrowser.qt import machinery
from qutebrowser.qt.core import QObject, QEvent, Qt, QTimer
from qutebrowser.qt.gui import QKeyEvent
from qutebrowser.qt.widgets import QWidget
from qutebrowser.config import config
from qutebrowser.utils import log, message, usertypes, qtutils, version, utils
from qutebrowser.keyinput import modeman, keyutils
from qutebrowser.utils import log, message, usertypes, qtutils
from qutebrowser.keyinput import modeman
class ChildEventFilter(QObject):
@ -38,8 +37,8 @@ class ChildEventFilter(QObject):
if event.type() == QEvent.Type.ChildAdded:
child = event.child()
if not isinstance(child, QWidget):
# Can e.g. happen when dragging text, or accessibility tree
# nodes since Qt 6.9
# Can e.g. happen when dragging text
log.misc.debug(f"Ignoring new child {qtutils.qobj_repr(child)}")
return False
log.misc.debug(
@ -55,30 +54,21 @@ class ChildEventFilter(QObject):
# - This is a child event filter on a tab (self._widget is not None)
# - We find an old existing child which is a QQuickWidget and is
# currently focused.
# - We're using an affected QtWebEngine version
# - We're using QtWebEngine >= 6.4 (older versions are not affected)
children = [
c for c in self._widget.findChildren(
QWidget, "", Qt.FindChildOption.FindDirectChildrenOnly)
if c is not child and
c.hasFocus() and
c.metaObject() is not None and
c.metaObject().className() == "QQuickWidget" # Qt 6.4+
c.metaObject().className() == "QQuickWidget"
]
if children and version.qtwebengine_versions().webengine < utils.VersionNumber(6, 6, 3):
if children:
log.misc.debug("Focusing new child")
child.setFocus()
child.installEventFilter(self._filter)
elif event.type() == QEvent.Type.ChildRemoved:
if isinstance(event, QKeyEvent):
# WORKAROUND for unknown (Py)Qt bug
info = keyutils.KeyInfo.from_event(event)
log.misc.warning(
f"ChildEventFilter: ignoring key event {info} "
f"on {qtutils.qobj_repr(obj)}"
)
return False
child = event.child()
log.misc.debug(
f"{qtutils.qobj_repr(obj)}: removed child {qtutils.qobj_repr(child)}")

View File

@ -11,7 +11,6 @@ from qutebrowser.qt.core import QUrl, QUrlQuery
from qutebrowser.utils import resources, javascript, jinja, standarddir, log, urlutils
from qutebrowser.config import config
from qutebrowser.misc import objects
_SYSTEM_PATHS = [
@ -128,12 +127,7 @@ def get_pdfjs_res_and_path(path):
content = None
file_path = None
if 'no-system-pdfjs' in objects.debug_flags:
system_paths = []
else:
system_paths = _SYSTEM_PATHS[:]
system_paths += [
system_paths = _SYSTEM_PATHS + [
# fallback
os.path.join(standarddir.data(), 'pdfjs'),
# hardcoded fallback for --temp-basedir

View File

@ -123,24 +123,25 @@ def data_for_url(url: QUrl) -> tuple[str, bytes]:
path = url.path()
host = url.host()
query = url.query()
# A url like "qute:foo" is split as "scheme:path", not "scheme:host".
log.misc.debug("url: {}, path: {}, host {}".format(
url.toDisplayString(), path, host))
if not path or not host:
new_url = QUrl()
new_url.setScheme('qute')
# When path is absent, e.g. qute://help (with no trailing slash)
if host:
new_url.setHost(host)
# When host is absent, e.g. qute:help
else:
new_url.setHost(path)
if not host:
# Redirect qute:help -> qute://help/
new_url = QUrl(url)
new_url.setHost(path)
new_url.setPath('/')
if not new_url.host(): # Valid path but not valid host
raise UrlInvalidError(f"Invalid host (from path): {path!r}")
raise Redirect(new_url)
if not path:
# Redirect qute://help -> qute://help/
new_url = QUrl(url)
new_url.setPath('/')
raise Redirect(new_url)
if query:
new_url.setQuery(query)
if new_url.host(): # path was a valid host
raise Redirect(new_url)
try:
handler = _HANDLERS[host]

View File

@ -286,10 +286,7 @@ class NotificationBridgePresenter(QObject):
if replaces_id is None:
if notification_id in self._active_notifications:
message.error(f"Got duplicate notification id {notification_id} "
f"from {self._adapter.NAME}")
self._drop_adapter()
return
raise Error(f"Got duplicate id {notification_id}")
qt_notification.show()
self._active_notifications[notification_id] = qt_notification

View File

@ -26,7 +26,7 @@ from qutebrowser.config import config, websettings
from qutebrowser.config.websettings import AttributeInfo as Attr
from qutebrowser.misc import pakjoy
from qutebrowser.utils import (standarddir, qtutils, message, log,
urlmatch, usertypes, objreg, version, utils)
urlmatch, usertypes, objreg, version)
if TYPE_CHECKING:
from qutebrowser.browser.webengine import interceptor
@ -378,12 +378,6 @@ 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():
@ -417,37 +411,6 @@ def _init_profile(profile: QWebEngineProfile) -> None:
lambda url: profile.clearVisitedLinks([url]))
_global_settings.init_settings()
_maybe_disable_hangouts_extension(profile)
def _maybe_disable_hangouts_extension(profile: QWebEngineProfile) -> None:
"""Disable the Hangouts extension for Qt 6.10+."""
if not config.val.qt.workarounds.disable_hangouts_extension:
return
if machinery.IS_QT6: # mypy
try:
ext_manager = profile.extensionManager()
except AttributeError:
return # added in QtWebEngine 6.10
qtwe_versions = version.qtwebengine_versions(avoid_init=True)
if (
qtwe_versions.webengine == utils.VersionNumber(6, 10, 1)
and profile.isOffTheRecord()
):
# WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/8785
log.misc.warning(
"Not disabling Hangouts extension on private profile to avoid "
"QtWebEngine crash with Qt 6.10.1")
return
assert ext_manager is not None # mypy
for info in ext_manager.extensions():
if info.id() == pakjoy.HANGOUTS_EXT_ID:
log.misc.debug(f"Disabling extension: {info.name()}")
# setExtensionEnabled(info, False) seems to segfault
ext_manager.unloadExtension(info)
def _clear_webengine_permissions_json():
@ -469,18 +432,14 @@ def _clear_webengine_permissions_json():
)
def default_qt_profile() -> QWebEngineProfile:
"""Get the default profile from Qt."""
if machinery.IS_QT6:
return QWebEngineProfile("Default")
else:
return QWebEngineProfile.defaultProfile()
def _init_default_profile():
"""Init the default QWebEngineProfile."""
global default_profile
default_profile = default_qt_profile()
if machinery.IS_QT6:
default_profile = QWebEngineProfile("Default")
else:
default_profile = QWebEngineProfile.defaultProfile()
assert not default_profile.isOffTheRecord()
assert parsed_user_agent is None # avoid earlier profile initialization
@ -488,19 +447,8 @@ def _init_default_profile():
init_user_agent()
ua_version = version.qtwebengine_versions()
logger = log.init.warning
if machinery.IS_QT5:
# With Qt 5.15, we can't quite be sure about which QtWebEngine patch version
# we're getting, as ELF parsing might be broken and there's no other way.
# For most of the code, we don't really care about the patch version though.
assert (
non_ua_version.webengine.strip_patch() == ua_version.webengine.strip_patch()
), (non_ua_version, ua_version)
logger = log.init.debug
if ua_version.webengine != non_ua_version.webengine:
logger(
log.init.warning(
"QtWebEngine version mismatch - unexpected behavior might occur, "
"please open a bug about this.\n"
f" Early version: {non_ua_version}\n"
@ -539,23 +487,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_short} "
# "{upstream_browser_key}/{upstream_browser_version} "
# "Safari/{webkit_version}")
firefox_ua = "Mozilla/5.0 ({os_info}; rv:145.0) Gecko/20100101 Firefox/145.0"
# Needed for gitlab.gnome.org which blocks old Chromium versions outright,
# except when QtWebEngine/... is in the UA.
#
# We could further modify the UA to just "qutebrowser" or something so we don't get
# Anubis at all, but it looks like their Anubis triggers to more than just
# Mozilla/5.0 (also AppleWebKit/... and Chromium/... possibly?), so at that point
# I'm not sure if we can strip down the UA so much without breaking
# something in GitLab as well.
not_mozilla_ua = (
"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}"
)
no_qtwe_ua = ("Mozilla/5.0 ({os_info}) "
"AppleWebKit/{webkit_version} (KHTML, like Gecko) "
"{upstream_browser_key}/{upstream_browser_version} "
"Safari/{webkit_version}")
firefox_ua = "Mozilla/5.0 ({os_info}; rv:133.0) Gecko/20100101 Firefox/133.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."""
@ -570,14 +508,23 @@ def _init_site_specific_quirks():
"Safari/537.36"
)
utils.unused(maybe_newer_chrome_ua)
user_agents = [
# Needed to avoid a ""WhatsApp works with Google Chrome 36+" error
# page which doesn't allow to use WhatsApp Web at all. Also see the
# additional JS quirk: qutebrowser/javascript/quirks/whatsapp_web.user.js
# https://github.com/qutebrowser/qutebrowser/issues/4445
("ua-whatsapp", 'https://web.whatsapp.com/', no_qtwe_ua),
# Needed to avoid a "you're using a browser [...] that doesn't allow us
# to keep your account secure" error.
# https://github.com/qutebrowser/qutebrowser/issues/5182
("ua-google", "https://accounts.google.com/*", firefox_ua),
("ua-gnome-gitlab", "https://gitlab.gnome.org/*", not_mozilla_ua),
("ua-google", 'https://accounts.google.com/*', firefox_ua),
# Needed because Slack adds an error which prevents using it relatively
# aggressively, despite things actually working fine.
# October 2023: Slack claims they only support 112+. On #7951 at least
# one user claims it still works fine on 108 based Qt versions.
("ua-slack", 'https://*.slack.com/*', maybe_newer_chrome_ua(112)),
]
for name, pattern, ua in user_agents:

View File

@ -13,7 +13,7 @@ import html as html_utils
from typing import cast, Union, Optional
from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl,
QObject, QByteArray, QTimer)
QObject, QByteArray)
from qutebrowser.qt.network import QAuthenticator
from qutebrowser.qt.webenginecore import QWebEnginePage, QWebEngineScript, QWebEngineHistory
@ -626,14 +626,7 @@ class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate):
return data
def deserialize(self, data):
try:
qtutils.deserialize(data, self._history)
except OSError:
dump = "\n".join(
bytes(line).hex(" ") for line in utils.chunk(bytes(data), 16)
)
log.webview.debug(f"Failed to deserialize history data:\n{dump}")
raise
qtutils.deserialize(data, self._history)
def _load_items_workaround(self, items):
"""WORKAROUND for session loading not working on Qt 5.15.
@ -940,10 +933,6 @@ class _WebEnginePermissions(QObject):
notif = miscwidgets.FullscreenNotification(self._widget)
notif.set_timeout(timeout)
notif.show()
# Restore keyboard focus to the tab. Setting a NoFocus policy
# for FullscreenNotification doesn't seem to work.
if self._widget.isVisible():
self._widget.setFocus()
@pyqtSlot(QUrl, 'QWebEnginePage::Feature')
def _on_feature_permission_requested(self, url, feature):
@ -1374,11 +1363,6 @@ class WebEngineTab(browsertab.AbstractTab):
self._widget.page().toHtml(callback)
def run_js_async(self, code, callback=None, *, world=None):
if sip.isdeleted(self._widget):
# https://github.com/qutebrowser/qutebrowser/issues/3895
log.misc.debug("run_js_async called on deleted tab")
return
world_id_type = Union[QWebEngineScript.ScriptWorldId, int]
if world is None:
world_id: world_id_type = QWebEngineScript.ScriptWorldId.ApplicationWorld
@ -1623,7 +1607,6 @@ class WebEngineTab(browsertab.AbstractTab):
def _on_navigation_request(self, navigation):
super()._on_navigation_request(navigation)
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-103778
local_schemes = {"qute", "file"}
qtwe_ver = version.qtwebengine_versions().webengine
if (
@ -1636,6 +1619,7 @@ class WebEngineTab(browsertab.AbstractTab):
(utils.VersionNumber(6, 2) <= qtwe_ver < utils.VersionNumber(6, 2, 5) or
utils.VersionNumber(6, 3) <= qtwe_ver < utils.VersionNumber(6, 3, 1))
):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-103778
log.webview.debug(
"Working around blocked request from local page "
f"{self.url().toDisplayString()}"
@ -1643,51 +1627,6 @@ class WebEngineTab(browsertab.AbstractTab):
navigation.accepted = False
self.load_url(navigation.url)
# WORKAROUND for QtWebEngine >= 6.2 not allowing form requests from
# qute:// to outside domains.
if (
qtwe_ver >= utils.VersionNumber(6, 2) and
self.url() == QUrl("qute://start/") and
navigation.navigation_type == navigation.Type.form_submitted and
navigation.url.matches(
QUrl(config.val.url.searchengines['DEFAULT']),
urlutils.FormatOption.REMOVE_QUERY)
):
log.webview.debug(
"Working around qute://start loading issue for "
f"{navigation.url.toDisplayString()}")
navigation.accepted = False
# Using QTimer.singleShot as WORKAROUND for this crashing otherwise
# with QtWebEngine 6.10: https://bugreports.qt.io/browse/QTBUG-140543
QTimer.singleShot(0, functools.partial(self.load_url, navigation.url))
# WORKAROUND for QtWebEngine 6.2 - 6.5 blocking back/forward navigation too
if (
utils.VersionNumber(6, 6) > qtwe_ver >= utils.VersionNumber(6, 2) and
self.url() == QUrl("qute://bookmarks/") and
navigation.navigation_type == navigation.Type.back_forward
):
log.webview.debug(
"Working around qute://bookmarks loading issue for "
f"{navigation.url.toDisplayString()}")
navigation.accepted = False
self.load_url(navigation.url)
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-140515
ua_setting = "content.headers.user_agent"
if (
navigation.accepted
and config.instance.get(ua_setting, navigation.url, fallback=False)
is not usertypes.UNSET
and navigation.navigation_type == usertypes.NavigationRequest.Type.redirect
and navigation.is_main_frame
and utils.VersionNumber(6, 5) <= qtwe_ver < utils.VersionNumber(6, 10, 1)
):
navigation.accepted = False
# Using QTimer.singleShot as WORKAROUND for this crashing otherwise
# with QtWebEngine 6.10: https://bugreports.qt.io/browse/QTBUG-140543
QTimer.singleShot(0, functools.partial(self.load_url, navigation.url))
if not navigation.accepted or not navigation.is_main_frame:
return

View File

@ -4,7 +4,7 @@
"""A model that proxies access to one or more completion categories."""
from typing import overload, Optional, Any
from typing import overload, Optional, Any, cast
from collections.abc import MutableSequence
from qutebrowser.qt import machinery
@ -91,14 +91,14 @@ class CompletionModel(QAbstractItemModel):
Return: The item flags, or Qt.ItemFlag.NoItemFlags on error.
"""
if not index.isValid():
return qtutils.maybe_cast(_FlagType, machinery.IS_QT5, Qt.ItemFlag.NoItemFlags)
return cast(_FlagType, Qt.ItemFlag.NoItemFlags)
if index.parent().isValid():
# item
return (Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable |
Qt.ItemFlag.ItemNeverHasChildren)
else:
# category
return qtutils.maybe_cast(_FlagType, machinery.IS_QT5, Qt.ItemFlag.NoItemFlags)
return cast(_FlagType, Qt.ItemFlag.NoItemFlags)
def index(self, row: int, col: int, parent: QModelIndex = QModelIndex()) -> QModelIndex:
"""Get an index into the model.

View File

@ -333,13 +333,8 @@ class Config(QObject):
pattern, hide_userconfig=hide_userconfig)
self.changed.emit(opt.name)
if pattern is not None:
log.config.debug("Config option changed: {} = {} for {}".format(
opt.name, value, pattern))
else:
log.config.debug("Config option changed: {} = {}".format(
opt.name, value))
log.config.debug("Config option changed: {} = {}".format(
opt.name, value))
def _check_yaml(self, opt: 'configdata.Option', save_yaml: bool) -> None:
"""Make sure the given option may be set in autoconfig.yml."""

View File

@ -356,8 +356,9 @@ class ConfigCommands:
raise cmdutils.CommandError(":config-list-remove can only be used "
"for lists")
converted = opt.typ.valtype.from_str(value)
with self._handle_config_error():
converted = opt.typ.valtype.from_str(value)
option_value = self._config.get_mutable_obj(option)
if converted not in option_value:

View File

@ -391,7 +391,7 @@ qt.workarounds.disable_accelerated_2d_canvas:
name: String
valid_values:
- always: Disable accelerated 2d canvas
- auto: Disable on Qt versions with known issues, enable otherwise
- auto: Disable on Qt6 < 6.6.0, enable otherwise
- never: Enable accelerated 2d canvas
default: auto
backend: QtWebEngine
@ -422,19 +422,6 @@ qt.workarounds.disable_hangouts_extension:
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.
qt.workarounds.disable_accessibility:
type:
name: String
valid_values:
- always: Disable renderer accessibility
- auto: Disable on Qt versions with known issues, enable otherwise
- never: Enable renderer accessibility
default: auto
backend: QtWebEngine
restart: true
desc: >-
Disable accessibility to avoid crashes on Qt 6.10.1.
## auto_save
auto_save.interval:
@ -669,9 +656,10 @@ content.site_specific_quirks.skip:
type:
name: FlagList
valid_values:
- ua-whatsapp
- ua-google
- ua-slack
- ua-googledocs
- ua-gnome-gitlab
- js-whatsapp-web
- js-discord
- js-string-replaceall
@ -764,7 +752,7 @@ content.headers.referer:
content.headers.user_agent:
default: 'Mozilla/5.0 ({os_info})
AppleWebKit/{webkit_version} (KHTML, like Gecko)
{upstream_browser_key}/{upstream_browser_version_short}
{qt_key}/{qt_version} {upstream_browser_key}/{upstream_browser_version}
Safari/{webkit_version}'
type:
name: FormatString
@ -776,7 +764,6 @@ content.headers.user_agent:
- qt_version
- upstream_browser_key
- upstream_browser_version
- upstream_browser_version_short
- qutebrowser_version
completions:
# See https://techblog.willshouse.com/2012/01/03/most-common-user-agents/
@ -786,14 +773,14 @@ content.headers.user_agent:
# Vim-protip: Place your cursor below this comment and run
# :r!python scripts/dev/ua_fetch.py
- - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"
- Chrome 142 macOS
(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
- Chrome 131 macOS
- - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/142.0.0.0 Safari/537.36"
- Chrome 142 Win10
like Gecko) Chrome/131.0.0.0 Safari/537.36"
- Chrome 131 Win10
- - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/142.0.0.0 Safari/537.36"
- Chrome 142 Linux
Gecko) Chrome/131.0.0.0 Safari/537.36"
- Chrome 131 Linux
supports_pattern: true
desc: |
User agent to send.
@ -808,15 +795,14 @@ content.headers.user_agent:
* `{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 default user agent of
QtWebKit/QtWebEngine, but with the `QtWebEngine/...` part removed for
increased compatibility.
The default value is equal to the unchanged user agent of
QtWebKit/QtWebEngine.
Note that the value read from JavaScript is always the global value.
Note that the value read from JavaScript is always the global value. With
QtWebEngine between 5.12 and 5.14 (inclusive), changing the value exposed
to JavaScript requires a restart.
content.host_blocking.enabled:
renamed: content.blocking.enabled
@ -1616,7 +1602,6 @@ fileselect.single_file.command:
- ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"]
- ['["xterm", "-e", "fff", "-p", "{}"]', "fff in xterm"]
- ['["xterm", "-e", "lf", "-selection-path", "{}"]', "lf in xterm"]
- ['["xterm", "-e", "yazi", "--chooser-file", "{}"]', "yazi in xterm"]
default: ['xterm', '-e', 'ranger', '--choosefile={}']
desc: >-
Command (and arguments) to use for selecting a single file in forms.
@ -1637,7 +1622,6 @@ fileselect.multiple_files.command:
- ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"]
- ['["xterm", "-e", "fff", "-p", "{}"]', "fff in xterm"]
- ['["xterm", "-e", "lf", "-selection-path", "{}"]', "lf in xterm"]
- ['["xterm", "-e", "yazi", "--chooser-file", "{}"]', "yazi in xterm"]
default: ['xterm', '-e', 'ranger', '--choosefiles={}']
desc: >-
Command (and arguments) to use for selecting multiple files in forms.
@ -1657,7 +1641,6 @@ fileselect.folder.command:
- ['["xterm", "-e", "ranger", "--choosedir={}"]', "Ranger in xterm"]
- ['["xterm", "-e", "vifm", "--choose-dir", "{}"]', "vifm in xterm"]
- ['["xterm", "-e", "nnn", "-p", "{}"]', "nnn in xterm"]
- ['["xterm", "-e", "yazi", "--cwd-file", "{}"]', "yazi in xterm"]
default: ['xterm', '-e', 'ranger', '--choosedir={}']
desc: >-
Command (and arguments) to use for selecting a single folder in forms.

View File

@ -183,9 +183,9 @@ class StateConfig(configparser.ConfigParser):
return
old_chromium_version_str = self['general'].get('chromium_version', None)
if old_chromium_version_str == "no" or old_chromium_version_str is None:
if old_chromium_version_str in ['no', None]:
old_qtwe_version = self['general'].get('qtwe_version', None)
if old_qtwe_version == "no" or old_qtwe_version is None:
if old_qtwe_version in ['no', None]:
return
try:

View File

@ -78,7 +78,7 @@ def qt_args(namespace: argparse.Namespace) -> list[str]:
return argv
def _qtwebengine_features( # noqa: C901
def _qtwebengine_features(
versions: version.WebEngineVersions,
special_flags: Sequence[str],
) -> tuple[Sequence[str], Sequence[str]]:
@ -154,16 +154,6 @@ def _qtwebengine_features( # noqa: C901
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-89740
disabled_features.append('InstalledApp')
if versions.webengine >= utils.VersionNumber(6, 7):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-132681
# TODO adjust if fixed in Qt 6.9.2+
disabled_features.append('DocumentPictureInPictureAPI')
if utils.VersionNumber(6, 9) <= versions.webengine < utils.VersionNumber(6, 10, 1):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-135787
# and https://bugreports.qt.io/browse/QTBUG-141096
disabled_features.append('PermissionElement')
if not config.val.input.media_keys:
disabled_features.append('HardwareMediaKeyHandling')
@ -181,7 +171,7 @@ def _get_pak_name(locale_name: str) -> str:
Based on Chromium's behavior in l10n_util::CheckAndResolveLocale:
https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc;l=344-428;drc=43d5378f7f363dab9271ca37774c71176c9e7b69
"""
if locale_name in {'en', 'en-POSIX', 'en-PH', 'en-LR'}:
if locale_name in {'en', 'en-PH', 'en-LR'}:
return 'en-US'
elif locale_name.startswith('en-'):
return 'en-GB'
@ -356,21 +346,7 @@ _WEBENGINE_SETTINGS: dict[str, dict[Any, Optional[_SettingValueType]]] = {
'qt.workarounds.disable_accelerated_2d_canvas': {
'always': '--disable-accelerated-2d-canvas',
'never': None,
'auto': lambda versions: '--disable-accelerated-2d-canvas'
if machinery.IS_QT6
and versions.webengine
and versions.webengine < utils.VersionNumber(6, 8, 2)
else None,
},
'qt.workarounds.disable_accessibility': {
'always': '--disable-renderer-accessibility',
'never': None,
# WORKAROUND for https://qt-project.atlassian.net/browse/QTBUG-142320
'auto': lambda versions: '--disable-renderer-accessibility'
if machinery.IS_QT6
and versions.webengine
and versions.webengine == utils.VersionNumber(6, 10, 1)
else None,
'auto': lambda _versions: '--disable-accelerated-2d-canvas' if machinery.IS_QT6 else None,
},
}

View File

@ -34,13 +34,6 @@ class UserAgent:
qt_key: str
qt_version: Optional[str]
@property
def upstream_browser_version_short(self) -> str:
"""Return a shortened version of the upstream browser version."""
major, *rest = self.upstream_browser_version.split('.')
shortened = [major] + ["0"] * (len(rest))
return ".".join(shortened)
@classmethod
def parse(cls, ua: str) -> 'UserAgent':
"""Parse a user agent string into its components."""
@ -214,7 +207,6 @@ def _format_user_agent(template: str, backend: usertypes.Backend) -> str:
qt_version=qVersion(),
upstream_browser_key=parsed.upstream_browser_key,
upstream_browser_version=parsed.upstream_browser_version,
upstream_browser_version_short=parsed.upstream_browser_version_short,
qutebrowser_version=qutebrowser.__version__,
)

View File

@ -92,7 +92,7 @@ li {
the required packages for pdf.js are also installed.
<br/>
The package is named
<a href="https://archlinux.org/packages/extra/any/pdfjs-legacy/"><b>pdfjs-legacy</b></a> on Archlinux
<a href="https://archlinux.org/packages/community/any/pdfjs/"><b>pdfjs</b></a> on Archlinux
and <a href="https://packages.debian.org/bullseye/libjs-pdf"><b>libjs-pdf</b></a> on Debian.
</li>

View File

@ -19,16 +19,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
return { promise, resolve, reject }
}
}
// Chromium 126 / QtWebEngine 6.9
// https://caniuse.com/mdn-api_url_parse_static
if (typeof URL.parse === "undefined") {
URL.parse = function(url, base) {
try {
return new URL(url, base);
} catch (ex) {
return null;
}
}
}
})();

View File

@ -191,51 +191,36 @@ window._qutebrowser.webelem = (function() {
}
}
// Recursively finds elements from DOM that have a shadowRoot
// and returns the shadow roots in a list
function find_shadow_roots(container = document) {
const roots = [];
for (const elem of container.querySelectorAll("*")) {
if (elem.shadowRoot) {
roots.push(elem.shadowRoot, ...find_shadow_roots(elem.shadowRoot));
}
}
return roots;
}
funcs.find_css = (selector, only_visible) => {
// Find all places where we need to look for elements:
const containers = [[document, null]];
// Same-domain iframes
for (const frame of Array.from(window.frames)) {
if (iframe_same_domain(frame)) {
containers.push([frame.document, frame]);
}
}
// Open shadow roots
for (const root of find_shadow_roots()) {
containers.push([root, null]);
let elems;
try {
elems = document.querySelectorAll(selector);
} catch (ex) {
return {"success": false, "error": ex.toString()};
}
// Then find elements in all of them
const elems = [];
for (const [container, frame] of containers) {
try {
for (const elem of container.querySelectorAll(selector)) {
elems.push([elem, frame]);
}
} catch (ex) {
return {"success": false, "error": ex.toString()};
}
}
// Finally, filter by visibility
const subelem_frames = window.frames;
const out = [];
for (const [elem, frame] of elems) {
if (!only_visible || is_visible(elem, frame)) {
out.push(serialize_elem(elem, frame));
for (let i = 0; i < elems.length; ++i) {
if (!only_visible || is_visible(elems[i])) {
out.push(serialize_elem(elems[i]));
}
}
// Recurse into frames and add them
for (let i = 0; i < subelem_frames.length; i++) {
if (iframe_same_domain(subelem_frames[i])) {
const frame = subelem_frames[i];
const subelems = frame.document.
querySelectorAll(selector);
for (let elem_num = 0; elem_num < subelems.length; ++elem_num) {
if (!only_visible ||
is_visible(subelems[elem_num], frame)) {
out.push(serialize_elem(subelems[elem_num], frame));
}
}
}
}

View File

@ -562,7 +562,7 @@ class MainWindow(QWidget):
self._completion.on_clear_completion_selection)
self.status.cmd.hide_completion.connect(
self._completion.hide)
self.status.release_focus.connect(self.tabbed_browser.on_release_focus)
self.status.cmd.hide_cmd.connect(self.tabbed_browser.on_release_focus)
def _set_decoration(self, hidden):
"""Set the visibility of the window decoration via Qt."""

View File

@ -342,9 +342,9 @@ class PromptContainer(QWidget):
"""Leave KEY_MODE whenever a prompt is aborted."""
try:
modeman.leave(self._win_id, key_mode, 'aborted', maybe=True)
except (objreg.RegistryUnavailableError, RuntimeError):
except objreg.RegistryUnavailableError:
# window was deleted: ignore
log.prompt.debug(f"Ignoring leaving {key_mode} as window was deleted")
pass
@pyqtSlot(usertypes.KeyMode)
def _on_prompt_done(self, key_mode):
@ -654,12 +654,6 @@ class FilenamePrompt(_BasePrompt):
"""A prompt for a filename."""
# Note: This *must* be a class variable! If it's not, for unknown reasons,
# we get a segfault in Qt/PyQt in QFileInfoGatherer::getInfo() if we have
# nested download prompts (i.e. trigger a download while a download prompt
# is open already).
_null_icon_provider = NullIconProvider()
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._init_texts(question)
@ -759,7 +753,7 @@ class FilenamePrompt(_BasePrompt):
self._file_model = QFileSystemModel(self)
# avoid icon and mime type lookups, they are slow in Qt6
self._file_model.setIconProvider(self._null_icon_provider)
self._file_model.setIconProvider(NullIconProvider())
self._file_view.setModel(self._file_model)
self._file_view.clicked.connect(self._insert_path)

View File

@ -140,12 +140,10 @@ class StatusBar(QWidget):
moved: Emitted when the statusbar has moved, so the completion widget
can move to the right position.
arg: The new position.
release_focus: Emitted just before the statusbar is hidden.
"""
resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint')
release_focus = pyqtSignal()
STYLESHEET = _generate_stylesheet()
@ -367,7 +365,6 @@ class StatusBar(QWidget):
def _hide_cmd_widget(self):
"""Show temporary text instead of command widget."""
log.statusbar.debug("Hiding cmd widget")
self.release_focus.emit()
self._stack.setCurrentWidget(self.txt)
self.maybe_hide()

View File

@ -4,8 +4,6 @@
"""The main tabbed browser widget."""
import os
import signal
import collections
import functools
import weakref
@ -1010,18 +1008,7 @@ class TabbedBrowser(QWidget):
browsertab.TerminationStatus.killed: "Renderer process was killed",
browsertab.TerminationStatus.unknown: "Renderer process did not start",
}
sig = None
try:
if os.WIFSIGNALED(code):
sig = signal.Signals(os.WTERMSIG(code))
except (AttributeError, ValueError):
pass
if sig is not None:
msg = messages[status] + f" (status {code}: {sig.name})"
else:
msg = messages[status] + f" (status {code})"
msg = messages[status] + f" (status {code})"
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91715
versions = version.qtwebengine_versions()

View File

@ -422,37 +422,6 @@ class _BackendProblemChecker:
raise utils.Unreachable
def _force_wayland_hardware_acceleration(self) -> None:
"""Set environment variable so hardware acceleration works on Wayland.
Set EGL_PLATFORM=wayland to force ANGLE to obtain EGL display connection
for wayland platform. Otherwise, the display connection for
EGL_DEFAULT_DISPLAY may belong to a platform which Nvidia's EGL driver
doesn't support. In case of unsupported platform, EGL may fallback to
Mesa software renderer (LLVMPipe) disabling hardware acceleration in
Chromium.
Equivalent to:
https://codereview.qt-project.org/c/qt/qtwebengine/+/663568
"""
if objects.qapp.platformName() != 'wayland':
return
versions = version.qtwebengine_versions(avoid_init=True)
if versions.webengine >= utils.VersionNumber(6, 10):
# Qt workaround is active
return
egl_platform_var = "EGL_PLATFORM"
egl_platform = os.environ.get(egl_platform_var)
if not egl_platform:
os.environ[egl_platform_var] = "wayland"
elif egl_platform != "wayland":
log.init.warning(
f"{egl_platform_var} environment variable is set to {egl_platform!r}. "
"This may break hardware rendering on Wayland."
)
def _assert_backend(self, backend: usertypes.Backend) -> None:
assert objects.backend == backend, objects.backend
@ -464,7 +433,6 @@ class _BackendProblemChecker:
self._handle_ssl_support()
self._handle_serviceworker_nuking()
self._check_software_rendering()
self._force_wayland_hardware_acceleration()
self._confirm_chromium_version_changes()
else:
self._assert_backend(usertypes.Backend.QtWebKit)

View File

@ -48,8 +48,8 @@ def parse_fatal_stacktrace(text):
lines = [
r'(?P<type>Fatal Python error|Windows fatal exception): (?P<msg>.*)',
r' *',
r'(Current )?[Tt]hread .* \(most recent call first\): *',
r' (File ".*", line \d+ in (?P<func>.*)|<no Python frame>)',
r'(Current )?[Tt]hread [^ ]* \(most recent call first\): *',
r' File ".*", line \d+ in (?P<func>.*)',
]
m = re.search('\n'.join(lines), text)
if m is None:
@ -58,7 +58,7 @@ def parse_fatal_stacktrace(text):
else:
msg = m.group('msg')
typ = m.group('type')
func = m.group('func') or ''
func = m.group('func')
if typ == 'Windows fatal exception':
msg = 'Windows ' + msg
return msg, func

View File

@ -9,7 +9,6 @@ import os.path
import sys
import bdb
import pdb # noqa: T002
import types
import signal
import argparse
import functools
@ -17,7 +16,7 @@ import threading
import faulthandler
import dataclasses
from typing import TYPE_CHECKING, Optional, cast
from collections.abc import Callable, MutableMapping
from collections.abc import MutableMapping
from qutebrowser.qt.core import (pyqtSlot, qInstallMessageHandler, QObject,
QSocketNotifier, QTimer, QUrl)
@ -325,9 +324,7 @@ class SignalHandler(QObject):
self._activated = False
self._orig_wakeup_fd: Optional[int] = None
self._handlers: dict[
signal.Signals, Callable[[int, Optional[types.FrameType]], None]
] = {
self._handlers = {
signal.SIGINT: self.interrupt,
signal.SIGTERM: self.interrupt,
}
@ -335,10 +332,8 @@ class SignalHandler(QObject):
"SIGHUP": self.reload_config,
}
for sig_str, handler in platform_dependant_handlers.items():
try:
if hasattr(signal.Signals, sig_str):
self._handlers[signal.Signals[sig_str]] = handler
except KeyError:
pass
def activate(self):
"""Set up signal handlers.

View File

@ -23,7 +23,6 @@ import datetime
from typing import NoReturn
try:
import tkinter
import tkinter.messagebox
except ImportError:
tkinter = None # type: ignore[assignment]

View File

@ -7,7 +7,7 @@
This entire file is a giant WORKAROUND for https://bugreports.qt.io/browse/QTBUG-114334.
"""
from typing import Union, Optional
from typing import Union, cast, Optional
import enum
import ctypes
import ctypes.util
@ -16,7 +16,7 @@ from qutebrowser.qt import sip, machinery
from qutebrowser.qt.core import QAbstractNativeEventFilter, QByteArray, qVersion
from qutebrowser.misc import objects
from qutebrowser.utils import log, qtutils
from qutebrowser.utils import log
# Needs to be saved to avoid garbage collection
@ -104,8 +104,8 @@ class NativeEventFilter(QAbstractNativeEventFilter):
#
# Tuple because PyQt uses the second value as the *result out-pointer, which
# according to the Qt documentation is only used on Windows.
_PASS_EVENT_RET = (False, qtutils.maybe_cast(_PointerRetType, machinery.IS_QT6, 0))
_FILTER_EVENT_RET = (True, qtutils.maybe_cast(_PointerRetType, machinery.IS_QT6, 0))
_PASS_EVENT_RET = (False, cast(_PointerRetType, 0))
_FILTER_EVENT_RET = (True, cast(_PointerRetType, 0))
def __init__(self) -> None:
super().__init__()
@ -137,9 +137,7 @@ class NativeEventFilter(QAbstractNativeEventFilter):
xcb.xcb_disconnect(conn)
def nativeEventFilter(
self,
evtype: Union[QByteArray, bytes, bytearray, memoryview],
message: Optional[sip.voidptr],
self, evtype: Union[bytes, QByteArray], message: Optional[sip.voidptr]
) -> tuple[bool, _PointerRetType]:
"""Handle XCB events."""
# We're only installed when the platform plugin is xcb

View File

@ -26,7 +26,6 @@ instead of crashing.
"""
import os
import sys
import shutil
import pathlib
import dataclasses
@ -36,17 +35,11 @@ from collections.abc import Iterator
from qutebrowser.config import config
from qutebrowser.misc import binparsing, objects
from qutebrowser.qt import core
from qutebrowser.utils import qtutils, standarddir, version, utils, log, message
from qutebrowser.qt.webenginecore import QWebEngineProfile
HANGOUTS_EXT_ID = "nkeimhogjdpnpccoofpliimaahmaaome"
HANGOUTS_MARKER = f"// Extension ID: {HANGOUTS_EXT_ID}".encode("utf-8")
HANGOUTS_MARKER = b"// Extension ID: nkeimhogjdpnpccoofpliimaahmaaome"
HANGOUTS_IDS = [
# Linux
47222, # QtWebEngine 6.9 Beta 3
43932, # QtWebEngine 6.9 Beta 1
43722, # QtWebEngine 6.8
41262, # QtWebEngine 6.7
36197, # QtWebEngine 6.6
@ -62,11 +55,7 @@ PAK_VERSION = 5
RESOURCES_ENV_VAR = "QTWEBENGINE_RESOURCES_PATH"
DISABLE_ENV_VAR = "QUTE_DISABLE_PAKJOY"
CACHE_DIR_NAME = "webengine_resources_pak_quirk"
PAK_FILENAME = (
"qtwebengine_resources.debug.pak"
if core.QLibraryInfo.isDebugBuild()
else "qtwebengine_resources.pak"
)
PAK_FILENAME = "qtwebengine_resources.pak"
TARGET_URL = b"https://*.google.com/*"
REPLACEMENT_URL = b"https://qute.invalid/*"
@ -231,7 +220,7 @@ def copy_webengine_resources() -> Optional[pathlib.Path]:
)
# https://github.com/qutebrowser/qutebrowser/issues/8257
or config.val.qt.workarounds.disable_hangouts_extension
) or hasattr(QWebEngineProfile, "extensionManager"): # Qt 6.10+
):
# No patching needed
return None
@ -312,16 +301,3 @@ def patch_webengine() -> Iterator[None]:
del os.environ[RESOURCES_ENV_VAR]
else:
os.environ[RESOURCES_ENV_VAR] = old_value
def main() -> None:
with open(sys.argv[1], "rb") as f:
parser = PakParser(f)
print(parser.manifest.decode("utf-8"))
print()
print(f"entry: {parser.manifest_entry}")
print(f"URL offset: {parser.find_patch_offset()}")
if __name__ == "__main__":
main()

View File

@ -1,325 +0,0 @@
# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""Utilities to get the name of the window manager (X11) / compositor (Wayland)."""
from typing import NewType
from collections.abc import Iterator
import ctypes
import socket
import struct
import pathlib
import dataclasses
import contextlib
import ctypes.util
class Error(Exception):
"""Base class for errors in this module."""
class _WaylandDisplayStruct(ctypes.Structure):
pass
_WaylandDisplay = NewType("_WaylandDisplay", "ctypes._Pointer[_WaylandDisplayStruct]")
def _load_library(name: str) -> ctypes.CDLL:
lib = ctypes.util.find_library(name)
if lib is None:
raise Error(f"{name} library not found")
try:
return ctypes.CDLL(lib)
except OSError as e:
raise Error(f"Failed to load {name} library: {e}")
def _pid_from_fd(fd: int) -> int:
"""Get the process ID from a file descriptor using SO_PEERCRED.
https://stackoverflow.com/a/35827184
"""
if not hasattr(socket, "SO_PEERCRED"):
raise Error("Missing socket.SO_PEERCRED")
# struct ucred {
# pid_t pid;
# uid_t uid;
#  gid_t gid;
# }; // where all of those are integers
ucred_format = "3i"
ucred_size = struct.calcsize(ucred_format)
try:
sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
except OSError as e:
raise Error(f"Error creating socket for fd {fd}: {e}")
try:
ucred = sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, ucred_size)
except OSError as e:
raise Error(f"Error getting SO_PEERCRED for fd {fd}: {e}")
finally:
sock.close()
pid, _uid, _gid = struct.unpack(ucred_format, ucred)
return pid
def _process_name_from_pid(pid: int) -> str:
"""Get the process name from a PID by reading /proc/[pid]/cmdline."""
proc_path = pathlib.Path(f"/proc/{pid}/cmdline")
try:
return proc_path.read_text(encoding="utf-8").replace("\0", " ").strip()
except OSError as e:
raise Error(f"Error opening {proc_path}: {e}")
@contextlib.contextmanager
def _wayland_display(wayland_client: ctypes.CDLL) -> Iterator[_WaylandDisplay]:
"""Context manager to connect to a Wayland display."""
wayland_client.wl_display_connect.argtypes = [ctypes.c_char_p] # name
wayland_client.wl_display_connect.restype = ctypes.POINTER(_WaylandDisplayStruct)
wayland_client.wl_display_disconnect.argtypes = [
ctypes.POINTER(_WaylandDisplayStruct)
]
wayland_client.wl_display_disconnect.restype = None
display = wayland_client.wl_display_connect(None)
if not display:
raise Error("Can't connect to display")
try:
yield display
finally:
wayland_client.wl_display_disconnect(display)
def _wayland_get_fd(wayland_client: ctypes.CDLL, display: _WaylandDisplay) -> int:
"""Get the file descriptor for the Wayland display."""
wayland_client.wl_display_get_fd.argtypes = [ctypes.POINTER(_WaylandDisplayStruct)]
wayland_client.wl_display_get_fd.restype = ctypes.c_int
fd = wayland_client.wl_display_get_fd(display)
if fd < 0:
raise Error(f"Failed to get Wayland display file descriptor: {fd}")
return fd
def wayland_compositor_name() -> str:
"""Get the name of the running Wayland compositor.
Approach based on:
https://stackoverflow.com/questions/69302630/wayland-client-get-compositor-name
"""
wayland_client = _load_library("wayland-client")
with _wayland_display(wayland_client) as display:
fd = _wayland_get_fd(wayland_client, display)
pid = _pid_from_fd(fd)
process_name = _process_name_from_pid(pid)
return process_name
@dataclasses.dataclass
class _X11Atoms:
NET_SUPPORTING_WM_CHECK: int
NET_WM_NAME: int
UTF8_STRING: int
class _X11DisplayStruct(ctypes.Structure):
pass
_X11Display = NewType("_X11Display", "ctypes._Pointer[_X11DisplayStruct]")
_X11Window = NewType("_X11Window", int)
@contextlib.contextmanager
def _x11_open_display(xlib: ctypes.CDLL) -> Iterator[_X11Display]:
"""Open a connection to the X11 display."""
xlib.XOpenDisplay.argtypes = [ctypes.c_char_p]
xlib.XOpenDisplay.restype = ctypes.POINTER(_X11DisplayStruct)
xlib.XCloseDisplay.argtypes = [ctypes.POINTER(_X11DisplayStruct)]
xlib.XCloseDisplay.restype = None
display = xlib.XOpenDisplay(None)
if not display:
raise Error("Cannot open display")
try:
yield display
finally:
xlib.XCloseDisplay(display)
def _x11_intern_atom(
xlib: ctypes.CDLL, display: _X11Display, name: bytes, only_if_exists: bool = True
) -> int:
"""Call xlib's XInternAtom function."""
xlib.XInternAtom.argtypes = [
ctypes.POINTER(_X11DisplayStruct), # Display
ctypes.c_char_p, # Atom name
ctypes.c_int, # Only if exists (bool)
]
xlib.XInternAtom.restype = ctypes.c_ulong
atom = xlib.XInternAtom(display, name, only_if_exists)
if atom == 0:
raise Error(f"Failed to intern atom: {name!r}")
return atom
@contextlib.contextmanager
def _x11_get_window_property(
xlib: ctypes.CDLL,
display: _X11Display,
*,
window: _X11Window,
prop: int,
req_type: int,
length: int,
offset: int = 0,
delete: bool = False,
) -> Iterator[tuple["ctypes._Pointer[ctypes.c_ubyte]", ctypes.c_ulong]]:
"""Call xlib's XGetWindowProperty function."""
ret_actual_type = ctypes.c_ulong()
ret_actual_format = ctypes.c_int()
ret_nitems = ctypes.c_ulong()
ret_bytes_after = ctypes.c_ulong()
ret_prop = ctypes.POINTER(ctypes.c_ubyte)()
xlib.XGetWindowProperty.argtypes = [
ctypes.POINTER(_X11DisplayStruct), # Display
ctypes.c_ulong, # Window
ctypes.c_ulong, # Property
ctypes.c_long, # Offset
ctypes.c_long, # Length
ctypes.c_int, # Delete (bool)
ctypes.c_ulong, # Required type (Atom)
ctypes.POINTER(ctypes.c_ulong), # return: Actual type (Atom)
ctypes.POINTER(ctypes.c_int), # return: Actual format
ctypes.POINTER(ctypes.c_ulong), # return: Number of items
ctypes.POINTER(ctypes.c_ulong), # return: Bytes after
ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte)), # return: Property value
]
xlib.XGetWindowProperty.restype = ctypes.c_int
result = xlib.XGetWindowProperty(
display,
window,
prop,
offset,
length,
delete,
req_type,
ctypes.byref(ret_actual_type),
ctypes.byref(ret_actual_format),
ctypes.byref(ret_nitems),
ctypes.byref(ret_bytes_after),
ctypes.byref(ret_prop),
)
if result != 0:
raise Error(f"XGetWindowProperty for {prop} failed: {result}")
if not ret_prop:
raise Error(f"Property {prop} is NULL")
if ret_actual_type.value != req_type:
raise Error(
f"Expected type {req_type}, got {ret_actual_type.value} for property {prop}"
)
if ret_bytes_after.value != 0:
raise Error(
f"Expected no bytes after property {prop}, got {ret_bytes_after.value}"
)
try:
yield ret_prop, ret_nitems
finally:
xlib.XFree(ret_prop)
def _x11_get_wm_window(
xlib: ctypes.CDLL, display: _X11Display, *, atoms: _X11Atoms
) -> _X11Window:
"""Get the _NET_SUPPORTING_WM_CHECK window."""
xlib.XDefaultScreen.argtypes = [ctypes.POINTER(_X11DisplayStruct)]
xlib.XDefaultScreen.restype = ctypes.c_int
xlib.XRootWindow.argtypes = [
ctypes.POINTER(_X11DisplayStruct), # Display
ctypes.c_int, # Screen number
]
xlib.XRootWindow.restype = ctypes.c_ulong
screen = xlib.XDefaultScreen(display)
root_window = xlib.XRootWindow(display, screen)
with _x11_get_window_property(
xlib,
display,
window=root_window,
prop=atoms.NET_SUPPORTING_WM_CHECK,
req_type=33, # XA_WINDOW
length=1,
) as (prop, _nitems):
win = ctypes.cast(prop, ctypes.POINTER(ctypes.c_ulong)).contents.value
return _X11Window(win)
def _x11_get_wm_name(
xlib: ctypes.CDLL,
display: _X11Display,
*,
atoms: _X11Atoms,
wm_window: _X11Window,
) -> str:
"""Get the _NET_WM_NAME property of the window manager."""
with _x11_get_window_property(
xlib,
display,
window=wm_window,
prop=atoms.NET_WM_NAME,
req_type=atoms.UTF8_STRING,
length=1024, # somewhat arbitrary
) as (prop, nitems):
if nitems.value <= 0:
raise Error(f"{nitems.value} items found in _NET_WM_NAME property")
wm_name = ctypes.string_at(prop, nitems.value).decode("utf-8")
if not wm_name:
raise Error("Window manager name is empty")
return wm_name
def x11_wm_name() -> str:
"""Get the name of the running X11 window manager."""
xlib = _load_library("X11")
with _x11_open_display(xlib) as display:
atoms = _X11Atoms(
NET_SUPPORTING_WM_CHECK=_x11_intern_atom(
xlib, display, b"_NET_SUPPORTING_WM_CHECK"
),
NET_WM_NAME=_x11_intern_atom(xlib, display, b"_NET_WM_NAME"),
UTF8_STRING=_x11_intern_atom(xlib, display, b"UTF8_STRING"),
)
wm_window = _x11_get_wm_window(xlib, display, atoms=atoms)
return _x11_get_wm_name(xlib, display, atoms=atoms, wm_window=wm_window)
if __name__ == "__main__":
try:
wayland_name = wayland_compositor_name()
print(f"Wayland compositor name: {wayland_name}")
except Error as e:
print(f"Wayland error: {e}")
try:
x11_name = x11_wm_name()
print(f"X11 window manager name: {x11_name}")
except Error as e:
print(f"X11 error: {e}")

View File

@ -172,14 +172,12 @@ def debug_flag_error(flag):
werror: Turn Python warnings into errors.
test-notification-service: Use the testing libnotify service.
caret: Enable debug logging for caret.js.
no-system-pdfjs: Ignore system-wide PDF.js installations
"""
valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history',
'no-scroll-filtering', 'log-requests', 'log-cookies',
'log-scroll-pos', 'log-sensitive-keys', 'stack', 'chromium',
'wait-renderer-process', 'avoid-chromium-init', 'werror',
'test-notification-service', 'log-qt-events', 'caret',
'no-system-pdfjs']
'test-notification-service', 'log-qt-events', 'caret']
if flag in valid_flags:
return flag

View File

@ -16,7 +16,6 @@ from typing import (
from collections.abc import Mapping, MutableSequence, Sequence, Callable
from qutebrowser.qt.core import Qt, QEvent, QMetaMethod, QObject, pyqtBoundSignal
from qutebrowser.qt.widgets import QApplication
from qutebrowser.utils import log, utils, qtutils, objreg
from qutebrowser.misc import objects
@ -346,9 +345,9 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
return wrapped
def _get_widgets(qapp: QApplication) -> Sequence[str]:
def _get_widgets() -> Sequence[str]:
"""Get a string list of all widgets."""
widgets = qapp.allWidgets()
widgets = objects.qapp.allWidgets()
widgets.sort(key=repr)
return [repr(w) for w in widgets]
@ -362,20 +361,17 @@ def _get_pyqt_objects(lines: MutableSequence[str],
_get_pyqt_objects(lines, kid, depth + 1)
def get_all_objects(start_obj: QObject = None, *, qapp: QApplication = None) -> str:
def get_all_objects(start_obj: QObject = None) -> str:
"""Get all children of an object recursively as a string."""
if qapp is None:
assert objects.qapp is not None
qapp = objects.qapp
output = ['']
widget_lines = _get_widgets(qapp)
widget_lines = _get_widgets()
widget_lines = [' ' + e for e in widget_lines]
widget_lines.insert(0, "Qt widgets - {} objects:".format(
len(widget_lines)))
output += widget_lines
if start_obj is None:
start_obj = qapp
start_obj = objects.qapp
pyqt_lines: list[str] = []
_get_pyqt_objects(pyqt_lines, start_obj)

View File

@ -127,9 +127,6 @@ def qt_message_handler(msg_type: qtcore.QtMsgType,
"QCoreApplication is created.",
# Qt 6.4 beta 1: https://bugreports.qt.io/browse/QTBUG-104741
"GL format 0 is not supported",
# WORKAROUND https://bugreports.qt.io/browse/QTBUG-137424
("QObject::disconnect: wildcard call disconnects from destroyed signal of "
"QNativeSocketEngine::unnamed"),
]
# not using utils.is_mac here, because we can't be sure we can successfully
# import the utils module here.

View File

@ -749,14 +749,3 @@ else:
return obj
QT_NONE: None = None
def maybe_cast(to_type: type[_T], do_cast: bool, value: Any) -> _T:
"""Cast `value` to `to_type` only if `do_cast` is true."""
if do_cast:
return cast(
to_type, # type: ignore[valid-type]
value,
)
return value

View File

@ -11,12 +11,18 @@ import ipaddress
import posixpath
import urllib.parse
import mimetypes
from typing import Optional, Union, cast
from typing import Optional, Union, cast, TYPE_CHECKING
from collections.abc import Iterable
from qutebrowser.qt import machinery
from qutebrowser.qt.core import QUrl, QUrlQuery
from qutebrowser.qt.network import QHostInfo, QHostAddress, QNetworkProxy
# WORKAROUND for
# https://www.riverbankcomputing.com/pipermail/pyqt/2024-December/046096.html
if TYPE_CHECKING and machinery.IS_QT6:
from qutebrowser.qt.core import QChar
else:
QChar = str
from qutebrowser.api import cmdutils
from qutebrowser.config import config
@ -698,7 +704,7 @@ def get_url_yank_text(url: QUrl, *, pretty: bool) -> str:
url_query = QUrlQuery()
url_query_str = url.query()
if '&' not in url_query_str and ';' in url_query_str:
url_query.setQueryDelimiters('=', ';')
url_query.setQueryDelimiters(cast(QChar, '='), cast(QChar, ';'))
url_query.setQuery(url_query_str)
for key in dict(url_query.queryItems()):
if key in config.val.url.yank_ignored_parameters:

View File

@ -722,11 +722,6 @@ def guess_mimetype(filename: str, fallback: bool = False) -> str:
fallback: Fall back to application/octet-stream if unknown.
"""
mimetype, _encoding = mimetypes.guess_type(filename)
if os.path.splitext(filename)[1] == '.mjs' and mimetype == "text/plain":
# Windows can sometimes have .mjs registered wrongly as text/plain:
# https://github.com/golang/go/issues/68591
return "text/javascript"
if mimetype is None:
if fallback:
return 'application/octet-stream'

View File

@ -44,7 +44,7 @@ except ImportError: # pragma: no cover
import qutebrowser
from qutebrowser.utils import (log, utils, standarddir, usertypes, message, resources,
qtutils)
from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin, elf, wmname
from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin, elf
from qutebrowser.browser import pdfjs
from qutebrowser.config import config
if TYPE_CHECKING:
@ -322,8 +322,8 @@ class ModuleInfo:
except (ImportError, ValueError):
self._installed = False
return
self._installed = True
else:
self._installed = True
for attribute_name in self._version_attributes:
if hasattr(module, attribute_name):
@ -332,13 +332,6 @@ class ModuleInfo:
self._version = str(version)
break
if self._version is None:
try:
self._version = importlib.metadata.version(self.name)
except importlib.metadata.PackageNotFoundError:
log.misc.debug(f"{self.name} not found")
self._version = None
self._initialized = True
def get_version(self) -> Optional[str]:
@ -379,7 +372,7 @@ class ModuleInfo:
version = self.get_version()
if version is None:
return f'{self.name}: unknown'
return f'{self.name}: yes'
text = f'{self.name}: {version}'
if self.is_outdated():
@ -390,7 +383,7 @@ class ModuleInfo:
def _create_module_info() -> dict[str, ModuleInfo]:
packages = [
('colorama', ['VERSION', '__version__']),
('jinja2', []),
('jinja2', ['__version__']),
('pygments', ['__version__']),
('yaml', ['__version__']),
('adblock', ['__version__'], "0.3.2"),
@ -494,7 +487,7 @@ def _pdfjs_version() -> str:
else:
pdfjs_file = pdfjs_file.decode('utf-8')
version_re = re.compile(
r"""^ *(PDFJS\.version|(var|const|\*) pdfjsVersion) = ['"]?(?P<version>[^'"\n]+)['"]?;?$""",
r"""^ *(PDFJS\.version|(var|const) pdfjsVersion) = ['"](?P<version>[^'"]+)['"];$""",
re.MULTILINE)
match = version_re.search(pdfjs_file)
@ -558,8 +551,6 @@ class WebEngineVersions:
112: '112.0.5615.213', # 2023-05-24, Qt 6.6
118: '118.0.5993.220', # 2024-01-25, Qt 6.7
122: '122.0.6261.171', # 2024-04-15, Qt 6.8
130: '130.0.6723.192', # 2025-01-06, Qt 6.9
134: '134.0.6998.208', # 2025-04-16, Qt 6.10
}
# Dates based on https://chromereleases.googleblog.com/
@ -602,7 +593,6 @@ class WebEngineVersions:
utils.VersionNumber(5, 15, 16): (_BASES[87], '119.0.6045.123'), # 2023-11-07
utils.VersionNumber(5, 15, 17): (_BASES[87], '123.0.6312.58'), # 2024-03-19
utils.VersionNumber(5, 15, 18): (_BASES[87], '130.0.6723.59'), # 2024-10-14
utils.VersionNumber(5, 15, 19): (_BASES[87], '135.0.7049.95'), # 2025-04-14
## Qt 6.2
@ -647,18 +637,6 @@ class WebEngineVersions:
## Qt 6.8
utils.VersionNumber(6, 8): (_BASES[122], '129.0.6668.70'), # 2024-09-24
utils.VersionNumber(6, 8, 1): (_BASES[122], '131.0.6778.70'), # 2024-11-12
utils.VersionNumber(6, 8, 2): (_BASES[122], '132.0.6834.111'), # 2025-01-22
utils.VersionNumber(6, 8, 3): (_BASES[122], '134.0.6998.89'), # 2025-03-10
## Qt 6.9
utils.VersionNumber(6, 9): (_BASES[130], '133.0.6943.141'), # 2025-02-25
utils.VersionNumber(6, 9, 1): (_BASES[130], '136.0.7103.114'), # 2025-05-13
utils.VersionNumber(6, 9, 2): (_BASES[130], '139.0.7258.67'), # 2025-07-29
utils.VersionNumber(6, 9, 3): (_BASES[130], '140.0.7339.207'), # 2025-09-22
## Qt 6.10
utils.VersionNumber(6, 10): (_BASES[134], '140.0.7339.207'), # 2025-09-22
utils.VersionNumber(6, 10, 1): (_BASES[134], '142.0.7444.162'), # 2025-11-11
}
def __post_init__(self) -> None:
@ -927,51 +905,6 @@ def _backend() -> str:
raise utils.Unreachable(objects.backend)
def _webengine_extensions() -> Sequence[str]:
"""Get a list of WebExtensions enabled in QtWebEngine."""
lines: list[str] = []
if (
objects.backend == usertypes.Backend.QtWebEngine
and machinery.IS_QT6 # mypy; TODO early return once Qt 5 is dropped
):
from qutebrowser.browser.webengine import webenginesettings
lines.append("WebExtensions:")
if webenginesettings.default_profile:
profile = webenginesettings.default_profile
elif "avoid-chromium-init" in objects.debug_flags:
lines[0] += " unknown (avoiding init)"
return lines
else:
profile = webenginesettings.default_qt_profile()
try:
ext_manager = profile.extensionManager()
except AttributeError:
# Added in QtWebEngine 6.10
return []
assert ext_manager is not None # mypy
if not ext_manager.extensions():
lines[0] += " none"
for info in ext_manager.extensions():
tags = [
("[x]" if info.isEnabled() else "[ ]") + " enabled",
("[x]" if info.isLoaded() else "[ ]") + " loaded",
("[x]" if info.isInstalled() else "[ ]") + " installed",
]
lines.append(f" {info.name()} ({info.id()})")
lines.append(f" {' '.join(tags)}")
lines.append(f" {info.path()}")
url = info.actionPopupUrl()
if url.isValid():
lines.append(f" {url.toDisplayString()}")
lines.append("")
return lines
def _uptime() -> datetime.timedelta:
time_delta = datetime.datetime.now() - objects.qapp.launch_time
# Round off microseconds
@ -1021,15 +954,13 @@ def version_info() -> str:
if QSslSocket.supportsSsl() else 'no'),
]
lines += _webengine_extensions()
if objects.qapp:
style = objects.qapp.style()
assert style is not None
metaobj = style.metaObject()
assert metaobj is not None
lines.append('Style: {}'.format(metaobj.className()))
lines.append('Qt Platform: {}'.format(gui_platform_info()))
lines.append('Platform plugin: {}'.format(objects.qapp.platformName()))
lines.append('OpenGL: {}'.format(opengl_info()))
importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__))
@ -1198,19 +1129,6 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover
old_context.makeCurrent(old_surface)
def gui_platform_info() -> str:
"""Get the Qt GUI platform name, optionally with the WM/compositor name."""
info = objects.qapp.platformName()
try:
if info == "xcb":
info += f" ({wmname.x11_wm_name()})"
elif info in ["wayland", "wayland-egl"]:
info += f" ({wmname.wayland_compositor_name()})"
except wmname.Error as e:
info += f" (Error: {e})"
return info
def pastebin_version(pbclient: pastebin.PastebinClient = None) -> None:
"""Pastebin the version and log the url to messages."""
def _yank_url(url: str) -> None:

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