Merge branch 'master' into docked-inspector

This commit is contained in:
Florian Bruhin 2020-06-18 13:48:19 +02:00
commit 1c01420aec
142 changed files with 3324 additions and 1514 deletions

View File

@ -10,8 +10,8 @@ image:
environment:
PYTHONUNBUFFERED: 1
PYTHON: C:\Python37-x64\python.exe
TESTENV: py37-pyqt514
PYTHON: C:\Python38-x64\python.exe
TESTENV: py38-pyqt514
install:
- '%PYTHON% --version'

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.11.1
current_version = 1.12.0
commit = True
message = Release v{new_version}
tag = True

View File

@ -1,16 +0,0 @@
---
name: ❓ Support Question
about: It's okay to ask questions via GitHub, but IRC/Reddit/Mailinglist might be better.
---
<!--
While it's fine to ask questions here, check the documentation for better
ways to get help:
https://github.com/qutebrowser/qutebrowser#getting-help
-->
**Version info (see `:version`)**:
**If applicable: Does the issue happen if you start with `--temp-basedir`?**:

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: ❓ Support Question
url: https://github.com/qutebrowser/qutebrowser/discussions
about: Use GitHub's new discussions feature for questions

33
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: "Code scanning"
on:
push:
pull_request:
schedule:
- cron: '0 3 * * 1'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: javascript, python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

View File

@ -45,7 +45,6 @@ matrix:
### PyQt 5.12 (Python 3.8)
- env: TESTENV=py38-pyqt512
# http://code.qt.io/cgit/qt/qtbase.git/commit/?id=c3a963da1f9e7b1d37e63eedded61da4fbdaaf9a
addons:
apt:
packages:
@ -53,20 +52,31 @@ matrix:
### PyQt 5.13 (Python 3.8)
- env: TESTENV=py38-pyqt513
# http://code.qt.io/cgit/qt/qtbase.git/commit/?id=c3a963da1f9e7b1d37e63eedded61da4fbdaaf9a
addons:
apt:
packages:
- libxkbcommon-x11-0
### PyQt 5.14 (Python 3.8, with coverage)
- env: TESTENV=py38-pyqt514-cov
# http://code.qt.io/cgit/qt/qtbase.git/commit/?id=c3a963da1f9e7b1d37e63eedded61da4fbdaaf9a
### PyQt 5.14 (Python 3.8)
- env: TESTENV=py38-pyqt514
addons:
apt:
packages:
- libxkbcommon-x11-0
### PyQt 5.15 (Python 3.8, with coverage)
- env: TESTENV=py38-pyqt515-cov
addons:
apt:
packages:
- libxkbcommon-x11-0
- libxcb-icccm4
- libxcb-image0
- libxcb-keysyms1
- libxcb-randr0
- libxcb-render-util0
- libxcb-xinerama0
### macOS Mojave (10.14)
- os: osx
env: TESTENV=py37-pyqt514 OSX=mojave

View File

@ -81,6 +81,11 @@ get sent to the general qutebrowser@ list).
If you're a reddit user, there's a
https://www.reddit.com/r/qutebrowser/[/r/qutebrowser] subreddit there.
Finally, qutebrowser is participating in the Beta for GitHub's new Discussions
feature, so you can also use the
https://github.com/qutebrowser/qutebrowser/discussions[discussions tab] on
GitHub to get in touch.
Contributions / Bugs
--------------------
@ -206,8 +211,8 @@ link:doc/backers.asciidoc[crowdfunding campaigns]!
Similar projects
----------------
Many projects with a similar goal as qutebrowser exist.
Most of them were inspirations for qutebrowser in some way, thanks for that!
Various projects with a similar goal like qutebrowser exist.
Many of them were inspirations for qutebrowser in some way, thanks for that!
Active
~~~~~~
@ -215,8 +220,9 @@ Active
* https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2)
* https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2)
* https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
* https://next.atlas.engineer/[next] (Lisp, Emacs-like but also offers Vim bindings, various backends - note there was a http://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution] which was handled quite badly)
* https://next.atlas.engineer/[next] (Lisp, Emacs-like but also offers Vim bindings, QtWebKit or GTK+/WebKit2 - note there was a http://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution] which was handled quite badly)
* https://github.com/parkouss/webmacs/[webmacs] (Python, Emacs-like with QtWebEngine)
* https://vieb.dev/[Vieb] (JavaScript, Electron)
* Chrome/Chromium addons:
https://vimium.github.io/[Vimium],
* Firefox addons (based on WebExtensions):

View File

@ -15,7 +15,90 @@ 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.
v1.12.0 (unreleased)
v1.13.0 (unreleased)
--------------------
Removed
~~~~~~~
- The `:debug-log-level` command was removed as it's replaced by the new
`logging.level.console` setting.
- The `qute://plainlog` special page got replaced by `qute://log?plain` - the
names of those pages is considered an implementation detail, and
`:messages --plain` should be used instead.
Changed
~~~~~~~
- New handling of bindings in hint mode which fixes various bugs and allows for
single-letter keybindings in hint mode.
- The `tor_identity` userscript now takes the password via a `-p` flag and has
a new `-c` flag to customize the Tor control port.
- `:config-write-py` now adds a note about `config.py` files being targeted at
advanced users.
- `:report` now takes two optional arguments for bug/contact information, so
that it can be used without the report window popping up.
- New `t[Cc][Hh]` default bindings which work similarly to the `t[Ss][Hh]`
bindings for JavaScript but toggle cookie permissions.
- The `:message` command now takes a `--logfilter` / `-f` argument, which is a
list of logging categories to show.
- The `:debug-log-filter` command now understands the full logfilter syntax.
- Changes to settings:
* `fonts.tabs` has been split into `fonts.tabs.{selected,unselected}` (see
below).
* `statusbar.hide` has been renamed to `statusbar.show` with the possible
values being `always` (`hide = False`), `never` (`hide = True`) or
`in-mode` (new, only show statusbar outside of normal mode.
* The `QtFont` config type formerly used for `fonts.tabs` and
`fonts.debug_console` is now removed and entirely replaced by `Font`. The
former distinction was mainly an implementation detail, and the accepted
values shouldn't have changed.
* `input.rocker_gestures` has been renamed to `input.mouse.rocker_gestures`.
* `content.dns_prefetch` is now enabled by default again, since the crashes
it caused are now fixed (Qt 5.15) or worked around.
- The statusbar now shows partial keychains in all modes (e.g. while hinting)
- Small performance improvements.
Added
~~~~~
- New settings:
* `logging.level.ram` and `logging.level.console` to configure the default
logging levels via the config.
* `fonts.tabs.selected` and `fonts.tabs.unselected` to set the font of the
selected tab independently from unselected tabs (e.g. to make it bold).
* `input.mouse.back_forward_buttons` which can be set to `false` to disable
back/forward mouse buttons.
Fixed
~~~~~
- Crash when `tabs.focus_stack_size` is set to -1.
- Crash when a `pdf.js` file for PDF.js exists, but `viewer.html` does not.
- Crash when `:completion-item-yank --sel` is used on a platform without
primary selection support (e.g. Windows/macOS).
- Crash when there's a feature permission request from Qt with an invalid URL
(which seems to happen with Qt 5.15 sometimes).
- Crash in rare cases where QtWebKit/QtWebEngine imports fail in unexpected
ways.
- Crash when something removed qutebrowser's IPC socket file and it's been
running for 6 hours.
- `:config-write-py` now works with paths starting with `~/...` again.
- New site-specific quirk for a missing `globalThis` in Qt <= 5.12 on Reddit
and Spotify.
- When `;` is added to `hints.chars`, using hint labels containing `;;` now
works properly.
- Hint letters outside of ASCII should now work.
- When `bindings.key_mappings` is used with hints, it now works properly with
letters outside of ASCII as well.
- With Qt 5.15, the audible/muted indicators are not updated properly due to a
Qt bug. This release adds a workaround so that at least the muted indicator
is shown properly.
- As a workaround for crashes with QtWebEngine versions between 5.12 and 5.14
(inclusive), changing the user agent (`content.headers.user_agent`) exposed
to JS now requires a restart. The corresponding HTTP header is not affected.
v1.12.0 (2020-06-01)
--------------------
Removed
@ -36,19 +119,26 @@ Added
Previously, that was only available as a separate application via `python3 -m
scripts.keytester`.
- New `:config-diff` command which opens the `qute://configdiff` page.
- New `--debug-flag log-cookies` to log cokies to the debug log for
debugging.
- New `--debug-flag log-cookies` to log cookies to the debug log.
- New `colors.contextmenu.disabled.{fg,bg}` settings to customize colors for
disabled items in the context menu.
- New line selection mode (`:toggle-selection --line`), bound to `Shift-V` in caret mode.
- New `colors.webpage.darkmode.*` settings to control Chromium's dark mode.
Note that those settings only work with QtWebEngine on Qt >= 5.14 and require
a restart of qutebrowser.
Changed
~~~~~~~
- Windows and macOS releases now ship Qt 5.15, which is based on Chromium
80.0.3987.163 with security fixes up to 81.0.4044.138.
- The `content.cookies.accept` setting now accepts URL patterns.
- Tests are now included in release tarballs. Note that only running them with
the exact dependencies listed in
`misc/requirements/requirements-tests.txt{,-raw}` is supported.
- The `:tab-focus` command now has completion for tabs in the current window.
- The `bindings.key_mappings` setting now maps `<Ctrl+I>` to the tab key by default.
- `:tab-give --private` now detaches a tab into a new private window.
Fixed
~~~~~
@ -59,6 +149,19 @@ Fixed
the main thread), qutebrowser did crash or freeze when trying to show its
exception handler. This is now fixed.
- `:inspector` now works correctly when cookies are disabled globally.
- Added workaround for a (Gentoo?) PyQt/packaging issue related to the
`QWebEngineFindTextResult` handling added in v1.11.0.
- When entering caret selection mode (`v, v`) very early before a page is
loaded, an error is now shown instead of a crash happening.
- The workaround for session loading with Qt 5.15 now handles
`sessions.lazy_restore` so that the saved page is loaded instead of the
"stub" page with no possibility to get to the web page.
- A site specific quirk to allow typing accented characters on Google
Docs was active for docs.google.com, but not drive.google.com. It is
now applied for both subdomains.
- With older graphics hardware (OpenGL < 4.3) with Qt 5.14 on Wayland, WebGL
causes segfaults. Now qutebrowser detects that combination and suggests to
disable WebGL or use XWayland.
v1.11.1 (2020-05-07)
--------------------

View File

@ -305,13 +305,13 @@ If you ever need to renew any of these certificates, you can take a look
at the currently imported certificates using:
+
----
certutil -D "sql:${HOME}/.pki/nssdb" -L
certutil -d "sql:${HOME}/.pki/nssdb" -L
----
+
Then remove the expired certificates using:
+
----
certutil -D "sql:${HOME}/.pki/nssdb" -D -n "My Fancy Certificate Nickname"
certutil -d "sql:${HOME}/.pki/nssdb" -D -n "My Fancy Certificate Nickname"
----
+
And then import the new and valid certificates using the procedure

View File

@ -286,8 +286,7 @@ Set all settings back to their default.
[[config-cycle]]
=== config-cycle
Syntax: +:config-cycle [*--pattern* 'pattern'] [*--temp*] [*--print*]
'option' ['values' ['values' ...]]+
Syntax: +:config-cycle [*--pattern* 'pattern'] [*--temp*] [*--print*] 'option' ['values' ['values' ...]]+
Cycle an option between multiple values.
@ -608,8 +607,7 @@ Show help about a command or setting.
[[hint]]
=== hint
Syntax: +:hint [*--mode* 'mode'] [*--add-history*] [*--rapid*] [*--first*]
['group'] ['target'] ['args' ['args' ...]]+
Syntax: +:hint [*--mode* 'mode'] [*--add-history*] [*--rapid*] [*--first*] ['group'] ['target'] ['args' ['args' ...]]+
Start hinting.
@ -811,7 +809,7 @@ Show a warning message in the statusbar.
[[messages]]
=== messages
Syntax: +:messages [*--plain*] [*--tab*] [*--bg*] [*--window*] ['level']+
Syntax: +:messages [*--plain*] [*--tab*] [*--bg*] [*--window*] [*--logfilter* 'logfilter'] ['level']+
Show a log of past messages.
@ -824,6 +822,9 @@ Show a log of past messages.
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in a new window.
* +*-f*+, +*--logfilter*+: A comma-separated filter string of logging categories. If the filter string starts with an exclamation mark, it
is negated.
[[navigate]]
=== navigate
@ -866,8 +867,7 @@ Do nothing.
[[open]]
=== open
Syntax: +:open [*--related*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*]
['url']+
Syntax: +:open [*--related*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*] ['url']+
Open a URL in the current/[count]th tab.
@ -1018,8 +1018,15 @@ Which count to pass the command.
[[report]]
=== report
Syntax: +:report ['info'] ['contact']+
Report a bug in qutebrowser.
==== positional arguments
* +'info'+: Information about the bug report. If given, no report dialog shows up.
* +'contact'+: Contact information for the report.
[[restart]]
=== restart
Restart qutebrowser while keeping existing tabs open.
@ -1197,9 +1204,7 @@ Load a session.
[[session-save]]
=== session-save
Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-window*]
[*--with-private*]
['name']+
Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-window*] [*--with-private*] ['name']+
Save a session.
@ -1263,9 +1268,7 @@ Set a mark at the current scroll position in the current tab.
[[spawn]]
=== spawn
Syntax: +:spawn [*--userscript*] [*--verbose*] [*--output*] [*--output-messages*]
[*--detach*]
'cmdline'+
Syntax: +:spawn [*--userscript*] [*--verbose*] [*--output*] [*--output-messages*] [*--detach*] 'cmdline'+
Spawn an external command.
@ -1348,7 +1351,7 @@ The tab index to focus, starting with 1.
[[tab-give]]
=== tab-give
Syntax: +:tab-give [*--keep*] ['win-id']+
Syntax: +:tab-give [*--keep*] [*--private*] ['win-id']+
Give the current tab to a new or existing window if win_id given.
@ -1359,6 +1362,7 @@ If no win_id is given, the tab will get detached into a new window.
==== optional arguments
* +*-k*+, +*--keep*+: If given, keep the old tab around.
* +*-p*+, +*--private*+: If the tab should be detached into a private instance.
==== count
Overrides win_id (index starts at 1 for win_id=0).
@ -1903,8 +1907,13 @@ This acts like readline's yank.
[[toggle-selection]]
=== toggle-selection
Syntax: +:toggle-selection [*--line*]+
Toggle caret selection mode.
==== optional arguments
* +*-l*+, +*--line*+: Enables line-selection.
== Debugging commands
These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag.
@ -1923,7 +1932,6 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|<<debug-keytester,debug-keytester>>|Show a keytester widget.
|<<debug-log-capacity,debug-log-capacity>>|Change the number of log lines to be stored in RAM.
|<<debug-log-filter,debug-log-filter>>|Change the log filter for console logging.
|<<debug-log-level,debug-log-level>>|Change the log level for console logging.
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page.
|<<debug-set-fake-clipboard,debug-set-fake-clipboard>>|Put data into the fake clipboard and enable logging, used for tests.
|<<debug-trace,debug-trace>>|Trace executed code via hunter.
@ -1998,15 +2006,6 @@ Change the log filter for console logging.
* +'filters'+: A comma separated list of logger names. Can also be "none" to clear any existing filters.
[[debug-log-level]]
=== debug-log-level
Syntax: +:debug-log-level 'level'+
Change the log level for console logging.
==== positional arguments
* +'level'+: The log level to set.
[[debug-pyeval]]
=== debug-pyeval
Syntax: +:debug-pyeval [*--file*] [*--quiet*] 's'+

View File

@ -111,6 +111,15 @@
|<<colors.tabs.selected.odd.bg,colors.tabs.selected.odd.bg>>|Background color of selected odd tabs.
|<<colors.tabs.selected.odd.fg,colors.tabs.selected.odd.fg>>|Foreground color of selected odd tabs.
|<<colors.webpage.bg,colors.webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color).
|<<colors.webpage.darkmode.algorithm,colors.webpage.darkmode.algorithm>>|Which algorithm to use for modifying how colors are rendered with darkmode.
|<<colors.webpage.darkmode.contrast,colors.webpage.darkmode.contrast>>|Contrast for dark mode.
|<<colors.webpage.darkmode.enabled,colors.webpage.darkmode.enabled>>|Render all web contents using a dark theme.
|<<colors.webpage.darkmode.grayscale.all,colors.webpage.darkmode.grayscale.all>>|Render all colors as grayscale.
|<<colors.webpage.darkmode.grayscale.images,colors.webpage.darkmode.grayscale.images>>|Desaturation factor for images in dark mode.
|<<colors.webpage.darkmode.policy.images,colors.webpage.darkmode.policy.images>>|Which images to apply dark mode to.
|<<colors.webpage.darkmode.policy.page,colors.webpage.darkmode.policy.page>>|Which pages to apply dark mode to.
|<<colors.webpage.darkmode.threshold.background,colors.webpage.darkmode.threshold.background>>|Threshold for inverting background elements with dark mode.
|<<colors.webpage.darkmode.threshold.text,colors.webpage.darkmode.threshold.text>>|Threshold for inverting text with dark mode.
|<<colors.webpage.prefers_color_scheme_dark,colors.webpage.prefers_color_scheme_dark>>|Force `prefers-color-scheme: dark` colors for websites.
|<<completion.cmd_history_max_items,completion.cmd_history_max_items>>|Number of commands to save in the command history.
|<<completion.delay,completion.delay>>|Delay (in milliseconds) before updating completions after typing a character.
@ -205,7 +214,8 @@
|<<fonts.messages.warning,fonts.messages.warning>>|Font used for warning messages.
|<<fonts.prompts,fonts.prompts>>|Font used for prompts.
|<<fonts.statusbar,fonts.statusbar>>|Font used in the statusbar.
|<<fonts.tabs,fonts.tabs>>|Font used in the tab bar.
|<<fonts.tabs.selected,fonts.tabs.selected>>|Font used for selected tabs.
|<<fonts.tabs.unselected,fonts.tabs.unselected>>|Font used for unselected tabs.
|<<fonts.web.family.cursive,fonts.web.family.cursive>>|Font family for cursive fonts.
|<<fonts.web.family.fantasy,fonts.web.family.fantasy>>|Font family for fantasy fonts.
|<<fonts.web.family.fixed,fonts.web.family.fixed>>|Font family for fixed fonts.
@ -242,12 +252,15 @@
|<<input.insert_mode.leave_on_load,input.insert_mode.leave_on_load>>|Leave insert mode when starting a new page load.
|<<input.insert_mode.plugins,input.insert_mode.plugins>>|Switch to insert mode when clicking flash and other plugins.
|<<input.links_included_in_focus_chain,input.links_included_in_focus_chain>>|Include hyperlinks in the keyboard focus chain when tabbing.
|<<input.mouse.back_forward_buttons,input.mouse.back_forward_buttons>>|Enable back and forward buttons on the mouse.
|<<input.mouse.rocker_gestures,input.mouse.rocker_gestures>>|Enable Opera-like mouse rocker gestures.
|<<input.partial_timeout,input.partial_timeout>>|Timeout (in milliseconds) for partially typed key bindings.
|<<input.rocker_gestures,input.rocker_gestures>>|Enable Opera-like mouse rocker gestures.
|<<input.spatial_navigation,input.spatial_navigation>>|Enable spatial navigation.
|<<keyhint.blacklist,keyhint.blacklist>>|Keychains that shouldn't be shown in the keyhint dialog.
|<<keyhint.delay,keyhint.delay>>|Time (in milliseconds) from pressing a key to seeing the keyhint dialog.
|<<keyhint.radius,keyhint.radius>>|Rounding radius (in pixels) for the edges of the keyhint dialog.
|<<logging.level.console,logging.level.console>>|Level for console (stdout/stderr) logs. Ignored if the `--loglevel` or `--debug` CLI flags are used.
|<<logging.level.ram,logging.level.ram>>|Level for in-memory logs.
|<<messages.timeout,messages.timeout>>|Duration (in milliseconds) to show messages in the statusbar for.
|<<new_instance_open_target,new_instance_open_target>>|How to open links in an existing instance if a new one is launched.
|<<new_instance_open_target_window,new_instance_open_target_window>>|Which window to choose when opening links as new tabs.
@ -268,9 +281,9 @@
|<<session.default_name,session.default_name>>|Name of the session to save by default.
|<<session.lazy_restore,session.lazy_restore>>|Load a restored tab as soon as it takes focus.
|<<spellcheck.languages,spellcheck.languages>>|Languages to use for spell checking.
|<<statusbar.hide,statusbar.hide>>|Hide the statusbar unless a message is shown.
|<<statusbar.padding,statusbar.padding>>|Padding (in pixels) for the statusbar.
|<<statusbar.position,statusbar.position>>|Position of the status bar.
|<<statusbar.show,statusbar.show>>|When to show the statusbar.
|<<statusbar.widgets,statusbar.widgets>>|List of widgets displayed in the statusbar.
|<<tabs.background,tabs.background>>|Open new tabs (middleclick/ctrl+click) in the background.
|<<tabs.close_mouse_button,tabs.close_mouse_button>>|Mouse button with which to close tabs.
@ -446,6 +459,7 @@ Default:
* +pass:[J]+: +pass:[scroll down]+
* +pass:[K]+: +pass:[scroll up]+
* +pass:[L]+: +pass:[scroll right]+
* +pass:[V]+: +pass:[toggle-selection --line]+
* +pass:[Y]+: +pass:[yank selection -s]+
* +pass:[[]+: +pass:[move-to-start-of-prev-block]+
* +pass:[]]+: +pass:[move-to-start-of-next-block]+
@ -590,6 +604,7 @@ Default:
* +pass:[Sq]+: +pass:[open qute://bookmarks]+
* +pass:[Ss]+: +pass:[open qute://settings]+
* +pass:[T]+: +pass:[tab-focus]+
* +pass:[V]+: +pass:[enter-mode caret ;; toggle-selection --line]+
* +pass:[ZQ]+: +pass:[quit]+
* +pass:[ZZ]+: +pass:[quit --save]+
* +pass:[[[]+: +pass:[navigate prev]+
@ -637,6 +652,9 @@ Default:
* +pass:[sk]+: +pass:[set-cmd-text -s :bind]+
* +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+
* +pass:[ss]+: +pass:[set-cmd-text -s :set]+
* +pass:[tCH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.cookies.accept all no-3rdparty never ;; reload]+
* +pass:[tCh]+: +pass:[config-cycle -p -u *://{url:host}/* content.cookies.accept all no-3rdparty never ;; reload]+
* +pass:[tCu]+: +pass:[config-cycle -p -u {url} content.cookies.accept all no-3rdparty never ;; reload]+
* +pass:[tIH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.images ;; reload]+
* +pass:[tIh]+: +pass:[config-cycle -p -u *://{url:host}/* content.images ;; reload]+
* +pass:[tIu]+: +pass:[config-cycle -p -u {url} content.images ;; reload]+
@ -646,6 +664,9 @@ Default:
* +pass:[tSH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.javascript.enabled ;; reload]+
* +pass:[tSh]+: +pass:[config-cycle -p -u *://{url:host}/* content.javascript.enabled ;; reload]+
* +pass:[tSu]+: +pass:[config-cycle -p -u {url} content.javascript.enabled ;; reload]+
* +pass:[tcH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.cookies.accept all no-3rdparty never ;; reload]+
* +pass:[tch]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.cookies.accept all no-3rdparty never ;; reload]+
* +pass:[tcu]+: +pass:[config-cycle -p -t -u {url} content.cookies.accept all no-3rdparty never ;; reload]+
* +pass:[th]+: +pass:[back -t]+
* +pass:[tiH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.images ;; reload]+
* +pass:[tih]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.images ;; reload]+
@ -739,6 +760,7 @@ Default:
- +pass:[&lt;Ctrl-6&gt;]+: +pass:[&lt;Ctrl-^&gt;]+
- +pass:[&lt;Ctrl-Enter&gt;]+: +pass:[&lt;Ctrl-Return&gt;]+
- +pass:[&lt;Ctrl-I&gt;]+: +pass:[&lt;Tab&gt;]+
- +pass:[&lt;Ctrl-J&gt;]+: +pass:[&lt;Return&gt;]+
- +pass:[&lt;Ctrl-M&gt;]+: +pass:[&lt;Return&gt;]+
- +pass:[&lt;Ctrl-[&gt;]+: +pass:[&lt;Escape&gt;]+
@ -1531,6 +1553,161 @@ Type: <<types,QtColor>>
Default: +pass:[white]+
[[colors.webpage.darkmode.algorithm]]
=== colors.webpage.darkmode.algorithm
Which algorithm to use for modifying how colors are rendered with darkmode.
This setting requires a restart.
Type: <<types,String>>
Valid values:
* +lightness-cielab+: Modify colors by converting them to CIELAB color space and inverting the L value.
* +lightness-hsl+: Modify colors by converting them to the HSL color space and inverting the lightness (i.e. the "L" in HSL).
* +brightness-rgb+: Modify colors by subtracting each of r, g, and b from their maximum value.
Default: +pass:[lightness-cielab]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.contrast]]
=== colors.webpage.darkmode.contrast
Contrast for dark mode.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to `lightness-hsl` or `brightness-rgb`.
This setting requires a restart.
Type: <<types,Float>>
Default: +pass:[0.0]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.enabled]]
=== colors.webpage.darkmode.enabled
Render all web contents using a dark theme.
Example configurations from Chromium's `chrome://flags`:
- "With simple HSL/CIELAB/RGB-based inversion": Set
`colors.webpage.darkmode.algorithm` accordingly.
- "With selective image inversion": Set
`colors.webpage.darkmode.policy.images` to `smart`.
- "With selective inversion of non-image elements": Set
`colors.webpage.darkmode.threshold.text` to 150 and
`colors.webpage.darkmode.threshold.background` to 205.
- "With selective inversion of everything": Combines the two variants
above.
This setting requires a restart.
Type: <<types,Bool>>
Default: +pass:[false]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.grayscale.all]]
=== colors.webpage.darkmode.grayscale.all
Render all colors as grayscale.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to `lightness-hsl` or `brightness-rgb`.
This setting requires a restart.
Type: <<types,Bool>>
Default: +pass:[false]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.grayscale.images]]
=== colors.webpage.darkmode.grayscale.images
Desaturation factor for images in dark mode.
If set to 0, images are left as-is. If set to 1, images are completely grayscale. Values between 0 and 1 desaturate the colors accordingly.
This setting requires a restart.
Type: <<types,Float>>
Default: +pass:[0.0]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.policy.images]]
=== colors.webpage.darkmode.policy.images
Which images to apply dark mode to.
WARNING: On Qt 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt].
This setting requires a restart.
Type: <<types,String>>
Valid values:
* +always+: Apply dark mode filter to all images.
* +never+: Never apply dark mode filter to any images.
* +smart+: Apply dark mode based on image content.
Default: +pass:[never]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.policy.page]]
=== colors.webpage.darkmode.policy.page
Which pages to apply dark mode to.
This setting requires a restart.
Type: <<types,String>>
Valid values:
* +always+: Apply dark mode filter to all frames, regardless of content.
* +smart+: Apply dark mode filter to frames based on background color.
Default: +pass:[smart]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.threshold.background]]
=== colors.webpage.darkmode.threshold.background
Threshold for inverting background elements with dark mode.
Background elements with brightness above this threshold will be inverted, and below it will be left as in the original, non-dark-mode page. Set to 256 to never invert the color or to 0 to always invert it.
Note: This behavior is the opposite of `colors.webpage.darkmode.threshold.text`!
This setting requires a restart.
Type: <<types,Int>>
Default: +pass:[0]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.threshold.text]]
=== colors.webpage.darkmode.threshold.text
Threshold for inverting text with dark mode.
Text colors with brightness below this threshold will be inverted, and above it will be left as in the original, non-dark-mode page. Set to 256 to always invert text color or to 0 to never invert text color.
This setting requires a restart.
Type: <<types,Int>>
Default: +pass:[256]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.prefers_color_scheme_dark]]
=== colors.webpage.prefers_color_scheme_dark
Force `prefers-color-scheme: dark` colors for websites.
@ -1760,6 +1937,7 @@ This setting is only available with the QtWebEngine backend.
[[content.cookies.accept]]
=== content.cookies.accept
Which cookies to accept.
With QtWebEngine, this setting also controls other features with tracking capabilities similar to those of cookies; including IndexedDB, DOM storage, filesystem API, service workers, and AppCache.
Note that with QtWebKit, only `all` and `never` are supported as per-domain values. Setting `no-3rdparty` or `no-unknown-3rdparty` per-domain on QtWebKit will have the same effect as `all`.
This setting supports URL patterns.
@ -1820,7 +1998,7 @@ This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
Default: +pass:[true]+
On QtWebEngine, this setting requires Qt 5.12 or newer.
@ -1937,7 +2115,9 @@ The following placeholders are defined:
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 URL patterns.
@ -2530,7 +2710,7 @@ Default: empty
=== fonts.debug_console
Font used for the debugging console.
Type: <<types,QtFont>>
Type: <<types,Font>>
Default: +pass:[default_size default_family]+
@ -2618,11 +2798,19 @@ Type: <<types,Font>>
Default: +pass:[default_size default_family]+
[[fonts.tabs]]
=== fonts.tabs
Font used in the tab bar.
[[fonts.tabs.selected]]
=== fonts.tabs.selected
Font used for selected tabs.
Type: <<types,QtFont>>
Type: <<types,Font>>
Default: +pass:[default_size default_family]+
[[fonts.tabs.unselected]]
=== fonts.tabs.unselected
Font used for unselected tabs.
Type: <<types,Font>>
Default: +pass:[default_size default_family]+
@ -3027,6 +3215,23 @@ Type: <<types,Bool>>
Default: +pass:[true]+
[[input.mouse.back_forward_buttons]]
=== input.mouse.back_forward_buttons
Enable back and forward buttons on the mouse.
Type: <<types,Bool>>
Default: +pass:[true]+
[[input.mouse.rocker_gestures]]
=== input.mouse.rocker_gestures
Enable Opera-like mouse rocker gestures.
This disables the context menu.
Type: <<types,Bool>>
Default: +pass:[false]+
[[input.partial_timeout]]
=== input.partial_timeout
Timeout (in milliseconds) for partially typed key bindings.
@ -3036,15 +3241,6 @@ Type: <<types,Int>>
Default: +pass:[5000]+
[[input.rocker_gestures]]
=== input.rocker_gestures
Enable Opera-like mouse rocker gestures.
This disables the context menu.
Type: <<types,Bool>>
Default: +pass:[false]+
[[input.spatial_navigation]]
=== input.spatial_navigation
Enable spatial navigation.
@ -3081,6 +3277,40 @@ Type: <<types,Int>>
Default: +pass:[6]+
[[logging.level.console]]
=== logging.level.console
Level for console (stdout/stderr) logs. Ignored if the `--loglevel` or `--debug` CLI flags are used.
Type: <<types,LogLevel>>
Valid values:
* +vdebug+
* +debug+
* +info+
* +warning+
* +error+
* +critical+
Default: +pass:[info]+
[[logging.level.ram]]
=== logging.level.ram
Level for in-memory logs.
Type: <<types,LogLevel>>
Valid values:
* +vdebug+
* +debug+
* +info+
* +warning+
* +error+
* +critical+
Default: +pass:[debug]+
[[messages.timeout]]
=== messages.timeout
Duration (in milliseconds) to show messages in the statusbar for.
@ -3373,14 +3603,6 @@ On QtWebEngine, this setting requires Qt 5.8 or newer.
On QtWebKit, this setting is unavailable.
[[statusbar.hide]]
=== statusbar.hide
Hide the statusbar unless a message is shown.
Type: <<types,Bool>>
Default: +pass:[false]+
[[statusbar.padding]]
=== statusbar.padding
Padding (in pixels) for the statusbar.
@ -3407,6 +3629,20 @@ Valid values:
Default: +pass:[bottom]+
[[statusbar.show]]
=== statusbar.show
When to show the statusbar.
Type: <<types,String>>
Valid values:
* +always+: Always show the statusbar.
* +never+: Always hide the statusbar.
* +in-mode+: Show the statusbar when in modes other than normal mode.
Default: +pass:[always]+
[[statusbar.widgets]]
=== statusbar.widgets
List of widgets displayed in the statusbar.
@ -3862,7 +4098,7 @@ characters in the search terms are replaced by safe characters (called
The search engine named `DEFAULT` is used when `url.auto_search` is turned
on and something else than a URL was entered to be opened. Other search
engines can be used by prepending the search engine name to the search
term, e.g. `:open google qutebrowser`.
term, e.g. `:open google qutebrowser`.
Type: <<types,Dict>>
@ -4003,6 +4239,7 @@ Lists with duplicate flags are invalid. Each item is checked against the valid v
When setting from a string, pass a json-like list, e.g. `["one", "two"]`.
|ListOrValue|A list of values, or a single value.
|LogLevel|A logging level.
|NewTabPosition|How new tabs are positioned.
|Padding|Setting for paddings around elements.
|Perc|A percentage.
@ -4015,9 +4252,6 @@ A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/
|QtColor|A color value.
A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)
|QtFont|A font family, with optional style/weight/size.
* Style: `normal`/`italic`/`oblique` * Weight: `normal`, `bold`, `100`..`900` * Size: _number_ `px`/`pt`
|Regex|A regular expression.
When setting from `config.py`, both a string or a `re.compile(...)` object are valid.

View File

@ -67,7 +67,7 @@ show it.
=== debug arguments
*-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}'::
Set loglevel
Override the configured console loglevel
*--logfilter* 'LOGFILTER'::
Comma-separated list of things to be logged to the debug log on stdout.

View File

@ -34,6 +34,27 @@ is available in the repositories:
# apt-get install python3-pyqt5-dbg python3-pyqt5.qtwebkit-dbg python3-dbg libqt5webkit5-dbg
----
Fedora
^^^^^^
For Fedora you first need to install the dnf/yum-utils:
----
# dnf install dnf-utils
----
Or:
----
# yum install yum-utils
----
Then install the needed debuginfo packages:
----
# debuginfo-install python3 qt5-qtwebengine python3-qt5-webengine python3-qt5-base python-qt5 python3-qt5 python3-qt5-webkit
----
Archlinux
^^^^^^^^^

View File

@ -44,6 +44,7 @@
</content_rating>
<releases>
<!-- Add new releases here -->
<release version="1.12.0" date="2020-06-01"/>
<release version="1.11.1" date="2020-05-07"/>
<release version="1.11.0" date="2020-04-27"/>
<release version="1.10.2" date="2020-04-17"/>

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
certifi==2020.4.5.1
certifi==2020.4.5.2
chardet==3.0.4
codecov==2.1.0
codecov==2.1.4
coverage==5.1
idna==2.9
requests==2.23.0

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
bump2version==1.0.0
certifi==2020.4.5.1
certifi==2020.4.5.2
cffi==1.14.0
chardet==3.0.4
colorama==0.4.3
@ -11,17 +11,16 @@ github3.py==1.3.0
hunter==3.1.3
idna==2.9
jwcrypto==0.7
lxml==4.5.0
manhole==1.6.0
packaging==20.3
packaging==20.4
pycparser==2.20
Pympler==0.8
pyparsing==2.4.7
PyQt-builder==1.3.2
PyQt-builder==1.4.0
python-dateutil==2.8.1
requests==2.23.0
sip==5.2.0
six==1.14.0
sip==5.3.0
six==1.15.0
toml==0.10.1
uritemplate==3.0.1
urllib3==1.25.9

View File

@ -4,5 +4,4 @@ pympler
github3.py
bump2version
requests
lxml
pyqt-builder

View File

@ -1,10 +1,10 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==19.3.0
flake8==3.8.1
flake8==3.8.2
flake8-bugbear==20.1.4
flake8-builtins==1.5.3
flake8-comprehensions==3.2.2
flake8-comprehensions==3.2.3
flake8-copyright==0.2.2
flake8-debugger==3.2.1
flake8-deprecated==1.3
@ -20,5 +20,5 @@ pep8-naming==0.10.0
pycodestyle==2.6.0
pydocstyle==5.0.2
pyflakes==2.2.0
six==1.14.0
six==1.15.0
snowballstemmer==2.0.0

View File

@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
mypy==0.770
mypy==0.780
mypy-extensions==0.4.3
-e git+https://github.com/stlehmann/PyQt5-stubs.git@master#egg=PyQt5_stubs
typed-ast==1.4.1

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
appdirs==1.4.4
packaging==20.3
packaging==20.4
pyparsing==2.4.7
setuptools==46.4.0
six==1.14.0
setuptools==47.1.1
six==1.15.0
wheel==0.34.2

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==2.3.3 # rq.filter: < 2.4
certifi==2020.4.5.1
certifi==2020.4.5.2
cffi==1.14.0
chardet==3.0.4
cryptography==2.9.2
@ -9,14 +9,14 @@ github3.py==1.3.0
idna==2.9
isort==4.3.21
jwcrypto==0.7
lazy-object-proxy==1.4.3
lazy-object-proxy==1.5.0
mccabe==0.6.1
pycparser==2.20
pylint==2.4.4 # rq.filter: < 2.5
python-dateutil==2.8.1
./scripts/dev/pylint_checkers
requests==2.23.0
six==1.14.0
six==1.15.0
typed-ast==1.4.1 ; python_version<"3.8"
uritemplate==3.0.1
urllib3==1.25.9

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.12.3 # rq.filter: < 5.13
PyQt5-sip==12.7.2
PyQt5-sip==12.8.0
PyQtWebEngine==5.12.1 # rq.filter: < 5.13

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.13.2 # rq.filter: < 5.14
PyQt5-sip==12.7.2
PyQt5-sip==12.8.0
PyQtWebEngine==5.13.2 # rq.filter: < 5.14

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.14.2 # rq.filter: < 5.15
PyQt5-sip==12.7.2
PyQt5-sip==12.8.0
PyQtWebEngine==5.14.0 # rq.filter: < 5.15

View File

@ -0,0 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.0 # rq.filter: < 6
PyQt5-sip==12.8.0
PyQtWebEngine==5.15.0 # rq.filter: < 6

View File

@ -0,0 +1,4 @@
#@ filter: PyQt5 < 6
#@ filter: PyQtWebEngine < 6
PyQt5 >= 5.15, < 6
PyQtWebEngine >= 5.15, < 6

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.14.2
PyQt5-sip==12.7.2
PyQtWebEngine==5.14.0
PyQt5==5.15.0
PyQt5-sip==12.8.0
PyQtWebEngine==5.15.0

View File

@ -2,21 +2,21 @@
alabaster==0.7.12
Babel==2.8.0
certifi==2020.4.5.1
certifi==2020.4.5.2
chardet==3.0.4
docutils==0.16
idna==2.9
imagesize==1.2.0
Jinja2==2.11.2
MarkupSafe==1.1.1
packaging==20.3
packaging==20.4
Pygments==2.6.1
pyparsing==2.4.7
pytz==2020.1
requests==2.23.0
six==1.14.0
six==1.15.0
snowballstemmer==2.0.0
Sphinx==3.0.3
Sphinx==3.1.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==1.0.3

View File

@ -10,15 +10,15 @@ EasyProcess==0.3
Flask==1.1.2
glob2==0.7
hunter==3.1.3
hypothesis==5.14.0
hypothesis==5.16.0
itsdangerous==1.1.0
jaraco.functools==3.0.1 ; python_version>="3.6"
# Jinja2==2.11.2
Mako==1.1.2
Mako==1.1.3
manhole==1.6.0
# MarkupSafe==1.1.1
more-itertools==8.3.0
packaging==20.3
packaging==20.4
parse==1.15.0
parse-type==0.5.2
pluggy==0.13.1
@ -26,22 +26,22 @@ py==1.8.1
py-cpuinfo==5.0.0
Pygments==2.6.1
pyparsing==2.4.7
pytest==5.4.2
pytest-bdd==3.3.0
pytest==5.4.3
pytest-bdd==3.4.0
pytest-benchmark==3.2.3
pytest-cov==2.8.1
pytest-cov==2.9.0
pytest-instafail==0.4.1.post0
pytest-mock==3.1.0
pytest-mock==3.1.1
pytest-qt==3.3.0
pytest-repeat==0.8.0
pytest-rerunfailures==9.0
pytest-travis-fold==1.3.0
pytest-xvfb==1.2.0
PyVirtualDisplay==0.2.5
six==1.14.0
sortedcontainers==2.1.0
PyVirtualDisplay==0.2.5 # rq.filter: < 1.0
six==1.15.0
sortedcontainers==2.2.2
soupsieve==2.0.1
vulture==1.4
wcwidth==0.1.9
vulture==1.5
wcwidth==0.2.4
Werkzeug==1.0.1
jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0

View File

@ -12,6 +12,8 @@ pytest-mock
pytest-qt
pytest-rerunfailures
pytest-xvfb
# https://github.com/The-Compiler/pytest-xvfb/issues/22
PyVirtualDisplay < 1.0
## optional:
# To test :debug-trace, gets skipped if hunter is not installed
@ -28,3 +30,4 @@ pytest-repeat
#@ markers: jaraco.functools python_version>="3.6"
#@ add: jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0
#@ ignore: Jinja2, MarkupSafe, colorama
#@ filter: PyVirtualDisplay < 1.0

View File

@ -3,13 +3,13 @@
appdirs==1.4.4
distlib==0.3.0
filelock==3.0.12
packaging==20.3
packaging==20.4
pluggy==0.13.1
py==1.8.1
pyparsing==2.4.7
six==1.14.0
six==1.15.0
toml==0.10.1
tox==3.15.0
tox==3.15.2
tox-pip-version==0.0.7
tox-venv==0.4.0
virtualenv==20.0.20
virtualenv==20.0.21

View File

@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
vulture==1.4
vulture==1.5

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2018 jnphilipp <mail@jnphilipp.org>
# Copyright 2018-2020 J. Nathanael Philipp (jnphilipp) <nathanael@philipp.land>
#
# This file is part of qutebrowser.
#
@ -30,6 +30,8 @@
import os
import sys
from argparse import ArgumentParser
try:
from stem import Signal
from stem.control import Controller
@ -41,12 +43,19 @@ except ImportError:
print('Failed to import stem.')
password = sys.argv[1]
with Controller.from_port(port=9051) as controller:
controller.authenticate(password)
controller.signal(Signal.NEWNYM)
if os.getenv('QUTE_FIFO'):
with open(os.environ['QUTE_FIFO'], 'w') as f:
f.write('message-info "Tor identity changed."')
else:
print('Tor identity changed.')
if __name__ == '__main__':
parser = ArgumentParser(prog='tor_identity')
parser.add_argument('-c', '--control-port', default=9051,
help='Tor control port (default 9051).')
parser.add_argument('-p', '--password', type=str, default=None,
help='Tor control port password.')
args = parser.parse_args()
with Controller.from_port(port=args.control_port) as controller:
controller.authenticate(args.password)
controller.signal(Signal.NEWNYM)
if os.getenv('QUTE_FIFO'):
with open(os.environ['QUTE_FIFO'], 'w') as f:
f.write('message-info "Tor identity changed."')
else:
print('Tor identity changed.')

View File

@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2020 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version__ = "1.11.1"
__version__ = "1.12.0"
__version_info__ = tuple(int(part) for part in __version__.split('.'))
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."

View File

@ -96,7 +96,7 @@ def run(args):
q_app.setApplicationVersion(qutebrowser.__version__)
if args.version:
print(version.version())
print(version.version_info())
sys.exit(usertypes.Exit.ok)
quitter.init(args)
@ -373,12 +373,23 @@ def open_desktopservices_url(url):
tabbed_browser.tabopen(url)
# This is effectively a @config.change_filter
# Howerver, logging is initialized too early to use that annotation
def _on_config_changed(name: str) -> None:
if name.startswith('logging.'):
log.init_from_config(config.val)
def _init_modules(*, args):
"""Initialize all 'modules' which need to be initialized.
Args:
args: The argparse namespace.
"""
log.init.debug("Initializing logging from config...")
log.init_from_config(config.val)
config.instance.changed.connect(_on_config_changed)
log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(q_app)
objreg.register('save-manager', save_manager)
@ -474,7 +485,9 @@ class Application(QApplication):
self._last_focus_object = None
qt_args = configinit.qt_args(args)
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
log.init.debug("Commandline args: {}".format(sys.argv[1:]))
log.init.debug("Parsed: {}".format(args))
log.init.debug("Qt arguments: {}".format(qt_args[1:]))
super().__init__(qt_args)
objects.args = args

View File

@ -429,13 +429,24 @@ class AbstractZoom(QObject):
self._set_factor_internal(self._zoom_factor)
class SelectionState(enum.Enum):
"""Possible states of selection in caret mode.
NOTE: Names need to line up with SelectionState in caret.js!
"""
none = 1
normal = 2
line = 3
class AbstractCaret(QObject):
"""Attribute ``caret`` of AbstractTab for caret browsing."""
#: Signal emitted when the selection was toggled.
#: (argument - whether the selection is now active)
selection_toggled = pyqtSignal(bool)
selection_toggled = pyqtSignal(SelectionState)
#: Emitted when a ``follow_selection`` action is done.
follow_selected_done = pyqtSignal()
@ -444,7 +455,6 @@ class AbstractCaret(QObject):
parent: QWidget = None) -> None:
super().__init__(parent)
self._widget = typing.cast(QWidget, None)
self.selection_enabled = False
self._mode_manager = mode_manager
mode_manager.entered.connect(self._on_mode_entered)
mode_manager.left.connect(self._on_mode_left)
@ -501,7 +511,7 @@ class AbstractCaret(QObject):
def move_to_end_of_document(self) -> None:
raise NotImplementedError
def toggle_selection(self) -> None:
def toggle_selection(self, line: bool = False) -> None:
raise NotImplementedError
def drop_selection(self) -> None:
@ -827,6 +837,15 @@ class AbstractTabPrivate:
def shutdown(self) -> None:
raise NotImplementedError
def run_js_sync(self, code: str) -> None:
"""Run javascript sync.
Result will be returned when running JS is complete.
This is only implemented for QtWebKit.
For QtWebEngine, always raises UnsupportedOperationError.
"""
raise NotImplementedError
class AbstractTab(QWidget):

View File

@ -453,7 +453,7 @@ class CommandDispatcher:
@cmdutils.argument('win_id', completion=miscmodels.window)
@cmdutils.argument('count', value=cmdutils.Value.count)
def tab_give(self, win_id: int = None, keep: bool = False,
count: int = None) -> None:
count: int = None, private: bool = False) -> None:
"""Give the current tab to a new or existing window if win_id given.
If no win_id is given, the tab will get detached into a new window.
@ -462,6 +462,7 @@ class CommandDispatcher:
win_id: The window ID of the window to give the current tab to.
keep: If given, keep the old tab around.
count: Overrides win_id (index starts at 1 for win_id=0).
private: If the tab should be detached into a private instance.
"""
if config.val.tabs.tabs_are_windows:
raise cmdutils.CommandError("Can't give tabs when using "
@ -479,7 +480,7 @@ class CommandDispatcher:
"only one tab")
tabbed_browser = self._new_tabbed_browser(
private=self._tabbed_browser.is_private)
private=private or self._tabbed_browser.is_private)
else:
if win_id not in objreg.window_registry:
raise cmdutils.CommandError(
@ -488,6 +489,10 @@ class CommandDispatcher:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if private and not tabbed_browser.is_private:
raise cmdutils.CommandError(
"The window with id {} is not private".format(win_id))
tabbed_browser.tabopen(self._current_url())
if not keep:
self._tabbed_browser.close_tab(self._current_widget(),
@ -1404,24 +1409,40 @@ class CommandDispatcher:
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', scope='window')
def messages(self, level='info', plain=False, tab=False, bg=False,
window=False):
@cmdutils.argument('logfilter', flag='f')
def messages(self, level='info', *, plain=False, tab=False, bg=False,
window=False, logfilter=None):
"""Show a log of past messages.
Args:
level: Include messages with `level` or higher severity.
Valid values: vdebug, debug, info, warning, error, critical.
plain: Whether to show plaintext (as opposed to html).
logfilter: A comma-separated filter string of logging categories.
If the filter string starts with an exclamation mark, it
is negated.
tab: Open in a new tab.
bg: Open in a background tab.
window: Open in a new window.
"""
if level.upper() not in log.LOG_LEVELS:
raise cmdutils.CommandError("Invalid log level {}!".format(level))
query = QUrlQuery()
query.addQueryItem('level', level)
if plain:
url = QUrl('qute://plainlog?level={}'.format(level))
else:
url = QUrl('qute://log?level={}'.format(level))
query.addQueryItem('plain', typing.cast(str, None))
if logfilter:
try:
log.LogFilter.parse(logfilter)
except log.InvalidLogFilterError as e:
raise cmdutils.CommandError(e)
query.addQueryItem('logfilter', logfilter)
url = QUrl('qute://log')
url.setQuery(query)
self._open(url, tab, bg, window)
def _open_editor_cb(self, elem):

View File

@ -131,7 +131,7 @@ class TabEventFilter(QObject):
Return:
True if the event should be filtered, False otherwise.
"""
is_rocker_gesture = (config.val.input.rocker_gestures and
is_rocker_gesture = (config.val.input.mouse.rocker_gestures and
e.buttons() == Qt.LeftButton | Qt.RightButton)
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
@ -219,7 +219,7 @@ class TabEventFilter(QObject):
Return:
True if the event should be filtered, False otherwise.
"""
return config.val.input.rocker_gestures
return config.val.input.mouse.rocker_gestures
def _handle_key_release(self, e):
"""Ignore repeated key release events going to the website.
@ -291,6 +291,11 @@ class TabEventFilter(QObject):
Return:
True if the event should be filtered, False otherwise.
"""
if (not config.val.input.mouse.back_forward_buttons and
e.button() in [Qt.XButton1, Qt.XButton2]):
# Back and forward on mice are disabled
return
if e.button() in [Qt.XButton1, Qt.LeftButton]:
# Back button on mice which have it, or rocker gesture
if self._tab.history.can_go_back():

View File

@ -30,6 +30,19 @@ from qutebrowser.misc import objects
from qutebrowser.config import config
_SYSTEM_PATHS = [
# Debian pdf.js-common
# Arch Linux pdfjs (AUR)
'/usr/share/pdf.js/',
# Flatpak (Flathub)
'/app/share/pdf.js/',
# Arch Linux pdf.js (AUR)
'/usr/share/javascript/pdf.js/',
# Debian libjs-pdf
'/usr/share/javascript/pdf/',
]
class PDFJSNotFound(Exception):
"""Raised when no pdf.js installation is found.
@ -130,16 +143,7 @@ def get_pdfjs_res_and_path(path):
content = None
file_path = None
system_paths = [
# Debian pdf.js-common
# Arch Linux pdfjs (AUR)
'/usr/share/pdf.js/',
# Flatpak (Flathub)
'/app/share/pdf.js/',
# Arch Linux pdf.js (AUR)
'/usr/share/javascript/pdf.js/',
# Debian libjs-pdf
'/usr/share/javascript/pdf/',
system_paths = _SYSTEM_PATHS + [
# fallback
os.path.join(standarddir.data(), 'pdfjs'),
# hardcoded fallback for --temp-basedir
@ -224,6 +228,7 @@ def is_available():
"""Return true if a pdfjs installation is available."""
try:
get_pdfjs_res('build/pdf.js')
get_pdfjs_res('web/viewer.html')
except PDFJSNotFound:
return False
else:

View File

@ -302,47 +302,50 @@ def qute_spawn_output(_url: QUrl) -> _HandlerRet:
def qute_version(_url):
"""Handler for qute://version."""
src = jinja.render('version.html', title='Version info',
version=version.version(),
version=version.version_info(),
copyright=qutebrowser.__copyright__)
return 'text/html', src
@add_handler('plainlog')
def qute_plainlog(url: QUrl) -> _HandlerRet:
"""Handler for qute://plainlog.
An optional query parameter specifies the minimum log level to print.
For example, qute://log?level=warning prints warnings and errors.
Level can be one of: vdebug, debug, info, warning, error, critical.
"""
if log.ram_handler is None:
text = "Log output was disabled."
else:
level = QUrlQuery(url).queryItemValue('level')
if not level:
level = 'vdebug'
text = log.ram_handler.dump_log(html=False, level=level)
src = jinja.render('pre.html', title='log', content=text)
return 'text/html', src
@add_handler('log')
def qute_log(url: QUrl) -> _HandlerRet:
"""Handler for qute://log.
An optional query parameter specifies the minimum log level to print.
There are three query parameters:
- level: The minimum log level to print.
For example, qute://log?level=warning prints warnings and errors.
Level can be one of: vdebug, debug, info, warning, error, critical.
- plain: If given (and not 'false'), plaintext is shown.
- logfilter: A filter string like the --logfilter commandline argument
accepts.
"""
query = QUrlQuery(url)
plain = (query.hasQueryItem('plain') and
query.queryItemValue('plain').lower() != 'false')
if log.ram_handler is None:
html_log = None
content = "Log output was disabled." if plain else None
else:
level = QUrlQuery(url).queryItemValue('level')
level = query.queryItemValue('level')
if not level:
level = 'vdebug'
html_log = log.ram_handler.dump_log(html=True, level=level)
src = jinja.render('log.html', title='log', content=html_log)
filter_str = query.queryItemValue('logfilter')
try:
logfilter = (log.LogFilter.parse(filter_str, only_debug=False)
if filter_str else None)
except log.InvalidLogFilterError as e:
raise UrlInvalidError(e)
content = log.ram_handler.dump_log(html=not plain,
level=level, logfilter=logfilter)
template = 'pre.html' if plain else 'log.html'
src = jinja.render(template, title='log', content=content)
return 'text/html', src

View File

@ -160,7 +160,7 @@ def ignore_certificate_errors(url, errors, abort_on):
True if the error should be ignored, False otherwise.
"""
ssl_strict = config.instance.get('content.ssl_strict', url=url)
log.webview.debug("Certificate errors {!r}, strict {}".format(
log.network.debug("Certificate errors {!r}, strict {}".format(
errors, ssl_strict))
for error in errors:
@ -186,7 +186,7 @@ def ignore_certificate_errors(url, errors, abort_on):
ignore = False
return ignore
elif ssl_strict is False:
log.webview.debug("ssl_strict is False, only warning about errors")
log.network.debug("ssl_strict is False, only warning about errors")
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/qutebrowser/qutebrowser/issues/114

View File

@ -154,7 +154,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
info.resourceType())
navigation_type_str = debug.qenum_key(QWebEngineUrlRequestInfo,
info.navigationType())
log.webview.debug("{} {}, first-party {}, resource {}, "
log.network.debug("{} {}, first-party {}, resource {}, "
"navigation {}".format(
bytes(info.requestMethod()).decode('ascii'),
info.requestUrl().toDisplayString(),
@ -164,7 +164,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
url = info.requestUrl()
first_party = info.firstPartyUrl()
if not url.isValid():
log.webview.debug("Ignoring invalid intercepted URL: {}".format(
log.network.debug("Ignoring invalid intercepted URL: {}".format(
url.errorString()))
return
@ -173,7 +173,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
try:
resource_type = self._resource_types[info.resourceType()]
except KeyError:
log.webview.warning(
log.network.warning(
"Resource type {} not found in RequestInterceptor dict."
.format(debug.qenum_key(QWebEngineUrlRequestInfo,
info.resourceType())))
@ -184,7 +184,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
if (first_party != QUrl('qute://settings/') or
info.resourceType() !=
QWebEngineUrlRequestInfo.ResourceTypeXhr):
log.webview.warning("Blocking malicious request from {} to {}"
log.network.warning("Blocking malicious request from {} to {}"
.format(first_party.toDisplayString(),
url.toDisplayString()))
info.block(True)

View File

@ -86,9 +86,9 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
return True
if initiator.isValid() and initiator.scheme() != 'qute':
log.misc.warning("Blocking malicious request from {} to {}".format(
initiator.toDisplayString(),
request_url.toDisplayString()))
log.network.warning("Blocking malicious request from {} to {}"
.format(initiator.toDisplayString(),
request_url.toDisplayString()))
job.fail(QWebEngineUrlRequestJob.RequestDenied)
return False
@ -119,7 +119,7 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
assert url.scheme() == 'qute'
log.misc.debug("Got request for {}".format(url.toDisplayString()))
log.network.debug("Got request for {}".format(url.toDisplayString()))
try:
mimetype, data = qutescheme.data_for_url(url)
except qutescheme.Error as e:
@ -136,14 +136,14 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
QWebEngineUrlRequestJob.RequestFailed,
}
exctype = type(e)
log.misc.error("{} while handling qute://* URL".format(
log.network.error("{} while handling qute://* URL".format(
exctype.__name__))
job.fail(errors[exctype])
except qutescheme.Redirect as e:
qtutils.ensure_valid(e.url)
job.redirect(e.url)
else:
log.misc.debug("Returning {} data".format(mimetype))
log.network.debug("Returning {} data".format(mimetype))
# We can't just use the QBuffer constructor taking a QByteArray,
# because that somehow segfaults...

View File

@ -326,8 +326,13 @@ def _update_settings(option):
"""Update global settings when qwebsettings changed."""
global_settings.update_setting(option)
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-75884
# (note this isn't actually fixed properly before Qt 5.15)
header_bug_fixed = (not qtutils.version_check('5.12', compiled=False) or
qtutils.version_check('5.15', compiled=False))
if option in ['content.headers.user_agent',
'content.headers.accept_language']:
'content.headers.accept_language'] and header_bug_fixed:
default_profile.setter.set_http_headers()
if private_profile:
private_profile.setter.set_http_headers()
@ -403,6 +408,7 @@ def _init_site_specific_quirks():
'https://accounts.google.com/*': firefox_ua,
'https://*.slack.com/*': new_chrome_ua,
'https://docs.google.com/*': firefox_ua,
'https://drive.google.com/*': firefox_ua,
}
if not qtutils.version_check('5.9'):

View File

@ -183,9 +183,23 @@ class _WebEngineSearchWrapHandler:
Args:
page: The QtWebEnginePage to connect to this handler.
"""
if qtutils.version_check("5.14"):
page.findTextFinished.connect(self._store_match_data)
self._nowrap_available = True
if not qtutils.version_check("5.14"):
return
try:
# pylint: disable=unused-import
from PyQt5.QtWebEngineCore import QWebEngineFindTextResult
except ImportError:
# WORKAROUND for some odd PyQt/packaging bug where the
# findTextResult signal is available, but QWebEngineFindTextResult
# is not. Seems to happen on e.g. Gentoo.
log.webview.warning("Could not import QWebEngineFindTextResult "
"despite running on Qt 5.14. You might need "
"to rebuild PyQtWebEngine.")
return
page.findTextFinished.connect(self._store_match_data)
self._nowrap_available = True
def _store_match_data(self, result):
"""Store information on the last match.
@ -381,7 +395,10 @@ class WebEngineCaret(browsertab.AbstractCaret):
if enabled is None:
log.webview.debug("Ignoring selection status None")
return
self.selection_toggled.emit(enabled)
if enabled:
self.selection_toggled.emit(browsertab.SelectionState.normal)
else:
self.selection_toggled.emit(browsertab.SelectionState.none)
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self, mode):
@ -436,8 +453,9 @@ class WebEngineCaret(browsertab.AbstractCaret):
def move_to_end_of_document(self):
self._js_call('moveToEndOfDocument')
def toggle_selection(self):
self._js_call('toggleSelection', callback=self.selection_toggled.emit)
def toggle_selection(self, line=False):
self._js_call('toggleSelection', line,
callback=self._toggle_sel_translate)
def drop_selection(self):
self._js_call('dropSelection')
@ -512,6 +530,13 @@ class WebEngineCaret(browsertab.AbstractCaret):
code = javascript.assemble('caret', command, *args)
self._tab.run_js_async(code, callback)
def _toggle_sel_translate(self, state_str):
if state_str is None:
message.error("Error toggling caret selection")
return
state = browsertab.SelectionState[state_str]
self.selection_toggled.emit(state)
class WebEngineScroller(browsertab.AbstractScroller):
@ -660,7 +685,11 @@ class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate):
if qtutils.version_check('5.15', compiled=False):
# WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/5359
if items:
self._tab.load_url(items[-1].url)
url = items[-1].url
if ((url.scheme(), url.host()) == ('qute', 'back') and
len(items) >= 2):
url = items[-2].url
self._tab.load_url(url)
return
if items:
@ -817,10 +846,15 @@ class WebEngineAudio(browsertab.AbstractAudio):
config.instance.changed.connect(self._on_config_changed)
def set_muted(self, muted: bool, override: bool = False) -> None:
was_muted = self.is_muted()
self._overridden = override
assert self._widget is not None
page = self._widget.page()
page.setAudioMuted(muted)
if was_muted != muted and qtutils.version_check('5.15'):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-85118
# so that the tab title at least updates the muted indicator
self.muted_changed.emit(muted)
def is_muted(self):
page = self._widget.page()
@ -940,9 +974,18 @@ class _WebEnginePermissions(QObject):
page.setFeaturePermission, url, feature,
QWebEnginePage.PermissionDeniedByUser)
permission_str = debug.qenum_key(QWebEnginePage, feature)
if not url.isValid():
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-85116
log.webview.warning("Ignoring feature permission {} for invalid "
"URL {}".format(permission_str, url))
deny_permission()
return
if feature not in self._options:
log.webview.error("Unhandled feature permission {}".format(
debug.qenum_key(QWebEnginePage, feature)))
permission_str))
deny_permission()
return
@ -1208,19 +1251,31 @@ class _WebEngineScripts(QObject):
"""Add site-specific quirk scripts.
NOTE: This isn't implemented for Qt 5.7 because of different UserScript
semantics there. We only have a quirk for WhatsApp Web right now. It
looks like that quirk isn't needed for Qt < 5.13.
semantics there. The WhatsApp Web quirk isn't needed for Qt < 5.13.
The globalthis_quirk would be, but let's not keep such old QtWebEngine
versions on life support.
"""
if not config.val.content.site_specific_quirks:
return
page_scripts = self._widget.page().scripts()
quirks = [
(
'whatsapp_web_quirk',
QWebEngineScript.DocumentReady,
QWebEngineScript.ApplicationWorld,
),
]
if not qtutils.version_check('5.13'):
quirks.append(('globalthis_quirk',
QWebEngineScript.DocumentCreation,
QWebEngineScript.MainWorld))
for filename in ['whatsapp_web_quirk']:
for filename, injection_point, world in quirks:
script = QWebEngineScript()
script.setName(filename)
script.setWorldId(QWebEngineScript.ApplicationWorld)
script.setInjectionPoint(QWebEngineScript.DocumentReady)
script.setWorldId(world)
script.setInjectionPoint(injection_point)
src = utils.read_file("javascript/{}.user.js".format(filename))
script.setSourceCode(src)
page_scripts.insert(script)
@ -1247,6 +1302,9 @@ class WebEngineTabPrivate(browsertab.AbstractTabPrivate):
self._tab.action.exit_fullscreen()
self._widget.shutdown()
def run_js_sync(self, code):
raise browsertab.UnsupportedOperationError
class WebEngineTab(browsertab.AbstractTab):
@ -1579,16 +1637,16 @@ class WebEngineTab(browsertab.AbstractTab):
url = error.url()
self._insecure_hosts.add(url.host())
log.webview.debug("Certificate error: {}".format(error))
log.network.debug("Certificate error: {}".format(error))
if error.is_overridable():
error.ignore = shared.ignore_certificate_errors(
url, [error], abort_on=[self.abort_questions])
else:
log.webview.error("Non-overridable certificate error: "
log.network.error("Non-overridable certificate error: "
"{}".format(error))
log.webview.debug("ignore {}, URL {}, requested {}".format(
log.network.debug("ignore {}, URL {}, requested {}".format(
error.ignore, url, self.url(requested=True)))
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-56207

View File

@ -19,6 +19,8 @@
"""The main browser widget for QtWebEngine."""
import typing
from PyQt5.QtCore import pyqtSignal, QUrl, PYQT_VERSION
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QWidget
@ -66,20 +68,26 @@ class WebEngineView(QWebEngineView):
However, it sometimes isn't, so we use this as a WORKAROUND for
https://bugreports.qt.io/browse/QTBUG-68727
This got introduced in Qt 5.11.0 and fixed in 5.12.0.
The above bug got introduced in Qt 5.11.0 and fixed in 5.12.0.
"""
if 'lost-focusproxy' not in objects.debug_flags:
proxy = self.focusProxy()
if proxy is not None:
return proxy
proxy = self.focusProxy() # type: typing.Optional[QWidget]
if 'lost-focusproxy' in objects.debug_flags:
proxy = None
if (proxy is not None or
not qtutils.version_check('5.11', compiled=False) or
qtutils.version_check('5.12', compiled=False)):
return proxy
# We don't want e.g. a QMenu.
rwhv_class = 'QtWebEngineCore::RenderWidgetHostViewQtDelegateWidget'
children = [c for c in self.findChildren(QWidget)
if c.isVisible() and c.inherits(rwhv_class)]
log.webview.debug("Found possibly lost focusProxy: {}"
.format(children))
if children:
log.webview.debug("Found possibly lost focusProxy: {}"
.format(children))
return children[-1] if children else None

View File

@ -105,8 +105,9 @@ def _is_secure_cipher(cipher):
def init():
"""Disable insecure SSL ciphers on old Qt versions."""
default_ciphers = QSslSocket.defaultCiphers()
log.init.debug("Default Qt ciphers: {}".format(
', '.join(c.name() for c in default_ciphers)))
log.init.vdebug( # type: ignore[attr-defined]
"Default Qt ciphers: {}".format(
', '.join(c.name() for c in default_ciphers)))
good_ciphers = []
bad_ciphers = []
@ -116,9 +117,10 @@ def init():
else:
bad_ciphers.append(cipher)
log.init.debug("Disabling bad ciphers: {}".format(
', '.join(c.name() for c in bad_ciphers)))
QSslSocket.setDefaultCiphers(good_ciphers)
if bad_ciphers:
log.init.debug("Disabling bad ciphers: {}".format(
', '.join(c.name() for c in bad_ciphers)))
QSslSocket.setDefaultCiphers(good_ciphers)
_SavedErrorsType = typing.MutableMapping[urlutils.HostTupleType,
@ -236,7 +238,7 @@ class NetworkManager(QNetworkAccessManager):
errors: A list of errors.
"""
errors = [certificateerror.CertificateErrorWrapper(e) for e in errors]
log.webview.debug("Certificate errors: {!r}".format(
log.network.debug("Certificate errors: {!r}".format(
' / '.join(str(err) for err in errors)))
try:
host_tpl = urlutils.host_tuple(
@ -252,7 +254,7 @@ class NetworkManager(QNetworkAccessManager):
is_rejected = set(errors).issubset(
self._rejected_ssl_errors[host_tpl])
log.webview.debug("Already accepted: {} / "
log.network.debug("Already accepted: {} / "
"rejected {}".format(is_accepted, is_rejected))
if is_rejected:
@ -425,7 +427,7 @@ class NetworkManager(QNetworkAccessManager):
if 'log-requests' in objects.debug_flags:
operation = debug.qenum_key(QNetworkAccessManager, op)
operation = operation.replace('Operation', '').upper()
log.webview.debug("{} {}, first-party {}".format(
log.network.debug("{} {}, first-party {}".format(
operation,
req.url().toDisplayString(),
current_url.toDisplayString()))

View File

@ -48,7 +48,7 @@ def handler(request, operation, current_url):
if ((url.scheme(), url.host(), url.path()) ==
('qute', 'settings', '/set')):
if current_url != QUrl('qute://settings/'):
log.webview.warning("Blocking malicious request from {} to {}"
log.network.warning("Blocking malicious request from {} to {}"
.format(current_url.toDisplayString(),
url.toDisplayString()))
return networkreply.ErrorNetworkReply(

View File

@ -25,9 +25,9 @@ import xml.etree.ElementTree
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWidgets import QWidget
from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab, shared
@ -184,14 +184,18 @@ class WebKitCaret(browsertab.AbstractCaret):
parent: QWidget = None) -> None:
super().__init__(mode_manager, parent)
self._tab = tab
self._selection_state = browsertab.SelectionState.none
@pyqtSlot(usertypes.KeyMode)
def _on_mode_entered(self, mode):
if mode != usertypes.KeyMode.caret:
return
self.selection_enabled = self._widget.hasSelection()
self.selection_toggled.emit(self.selection_enabled)
if self._widget.hasSelection():
self._selection_state = browsertab.SelectionState.normal
else:
self._selection_state = browsertab.SelectionState.none
self.selection_toggled.emit(self._selection_state)
settings = self._widget.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
@ -206,7 +210,7 @@ class WebKitCaret(browsertab.AbstractCaret):
#
# Note: We can't use hasSelection() here, as that's always
# true in caret mode.
if not self.selection_enabled:
if self._selection_state is browsertab.SelectionState.none:
self._widget.page().currentFrame().evaluateJavaScript(
utils.read_file('javascript/position_caret.js'))
@ -214,151 +218,189 @@ class WebKitCaret(browsertab.AbstractCaret):
def _on_mode_left(self, _mode):
settings = self._widget.settings()
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
if self.selection_enabled and self._widget.hasSelection():
if (self._selection_state is not browsertab.SelectionState.none and
self._widget.hasSelection()):
# Remove selection if it exists
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
self.selection_enabled = False
self._selection_state = browsertab.SelectionState.none
def move_to_next_line(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToNextLine
else:
if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectNextLine
else:
act = QWebPage.MoveToNextLine
for _ in range(count):
self._widget.triggerPageAction(act)
if self._selection_state is browsertab.SelectionState.line:
self._select_line_to_end()
def move_to_prev_line(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToPreviousLine
else:
if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectPreviousLine
else:
act = QWebPage.MoveToPreviousLine
for _ in range(count):
self._widget.triggerPageAction(act)
if self._selection_state is browsertab.SelectionState.line:
self._select_line_to_start()
def move_to_next_char(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToNextChar
else:
if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectNextChar
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = QWebPage.MoveToNextChar
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_prev_char(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToPreviousChar
else:
if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectPreviousChar
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = QWebPage.MoveToPreviousChar
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_end_of_word(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextWord]
if utils.is_windows: # pragma: no cover
act.append(QWebPage.MoveToPreviousChar)
else:
if self._selection_state is browsertab.SelectionState.normal:
act = [QWebPage.SelectNextWord]
if utils.is_windows: # pragma: no cover
act.append(QWebPage.SelectPreviousChar)
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = [QWebPage.MoveToNextWord]
if utils.is_windows: # pragma: no cover
act.append(QWebPage.MoveToPreviousChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_next_word(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextWord]
if not utils.is_windows: # pragma: no branch
act.append(QWebPage.MoveToNextChar)
else:
if self._selection_state is browsertab.SelectionState.normal:
act = [QWebPage.SelectNextWord]
if not utils.is_windows: # pragma: no branch
act.append(QWebPage.SelectNextChar)
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = [QWebPage.MoveToNextWord]
if not utils.is_windows: # pragma: no branch
act.append(QWebPage.MoveToNextChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_prev_word(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToPreviousWord
else:
if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectPreviousWord
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = QWebPage.MoveToPreviousWord
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_start_of_line(self):
if not self.selection_enabled:
act = QWebPage.MoveToStartOfLine
else:
if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectStartOfLine
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = QWebPage.MoveToStartOfLine
self._widget.triggerPageAction(act)
def move_to_end_of_line(self):
if not self.selection_enabled:
act = QWebPage.MoveToEndOfLine
else:
if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectEndOfLine
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = QWebPage.MoveToEndOfLine
self._widget.triggerPageAction(act)
def move_to_start_of_next_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextLine,
QWebPage.MoveToStartOfBlock]
else:
if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectNextLine,
QWebPage.SelectStartOfBlock]
else:
act = [QWebPage.MoveToNextLine,
QWebPage.MoveToStartOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
if self._selection_state is browsertab.SelectionState.line:
self._select_line_to_end()
def move_to_start_of_prev_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToPreviousLine,
QWebPage.MoveToStartOfBlock]
else:
if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectPreviousLine,
QWebPage.SelectStartOfBlock]
else:
act = [QWebPage.MoveToPreviousLine,
QWebPage.MoveToStartOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
if self._selection_state is browsertab.SelectionState.line:
self._select_line_to_start()
def move_to_end_of_next_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextLine,
QWebPage.MoveToEndOfBlock]
else:
if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectNextLine,
QWebPage.SelectEndOfBlock]
else:
act = [QWebPage.MoveToNextLine,
QWebPage.MoveToEndOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
if self._selection_state is browsertab.SelectionState.line:
self._select_line_to_end()
def move_to_end_of_prev_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock]
else:
if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock]
else:
act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
if self._selection_state is browsertab.SelectionState.line:
self._select_line_to_start()
def move_to_start_of_document(self):
if not self.selection_enabled:
act = QWebPage.MoveToStartOfDocument
else:
if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectStartOfDocument
else:
act = QWebPage.MoveToStartOfDocument
self._widget.triggerPageAction(act)
if self._selection_state is browsertab.SelectionState.line:
self._select_line()
def move_to_end_of_document(self):
if not self.selection_enabled:
act = QWebPage.MoveToEndOfDocument
else:
if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectEndOfDocument
else:
act = QWebPage.MoveToEndOfDocument
self._widget.triggerPageAction(act)
def toggle_selection(self):
self.selection_enabled = not self.selection_enabled
self.selection_toggled.emit(self.selection_enabled)
def toggle_selection(self, line=False):
if line:
self._selection_state = browsertab.SelectionState.line
self._select_line()
self.reverse_selection()
self._select_line()
self.reverse_selection()
elif self._selection_state is not browsertab.SelectionState.normal:
self._selection_state = browsertab.SelectionState.normal
else:
self._selection_state = browsertab.SelectionState.none
self.selection_toggled.emit(self._selection_state)
def drop_selection(self):
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
@ -375,6 +417,32 @@ class WebKitCaret(browsertab.AbstractCaret):
);
}""")
def _select_line(self):
self._widget.triggerPageAction(QWebPage.SelectStartOfLine)
self.reverse_selection()
self._widget.triggerPageAction(QWebPage.SelectEndOfLine)
self.reverse_selection()
def _select_line_to_end(self):
# direction of selection (if anchor is to the left or right
# of focus) has to be checked before moving selection
# to the end of line
if self._js_selection_left_to_right():
self._widget.triggerPageAction(QWebPage.SelectEndOfLine)
def _select_line_to_start(self):
if not self._js_selection_left_to_right():
self._widget.triggerPageAction(QWebPage.SelectStartOfLine)
def _js_selection_left_to_right(self):
"""Return True iff the selection's direction is left to right."""
return self._tab.private_api.run_js_sync("""
var sel = window.getSelection();
var position = sel.anchorNode.compareDocumentPosition(sel.focusNode);
(!position && sel.anchorOffset < sel.focusOffset ||
position === Node.DOCUMENT_POSITION_FOLLOWING);
""")
def _follow_selected(self, *, tab=False):
if QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled):
@ -710,6 +778,11 @@ class WebKitTabPrivate(browsertab.AbstractTabPrivate):
def shutdown(self):
self._widget.shutdown()
def run_js_sync(self, code):
document_element = self._widget.page().mainFrame().documentElement()
result = document_element.evaluateJavaScript(code)
return result
class WebKitTab(browsertab.AbstractTab):
@ -771,8 +844,7 @@ class WebKitTab(browsertab.AbstractTab):
def run_js_async(self, code, callback=None, *, world=None):
if world is not None and world != usertypes.JsWorld.jseval:
log.webview.warning("Ignoring world ID {}".format(world))
document_element = self._widget.page().mainFrame().documentElement()
result = document_element.evaluateJavaScript(code)
result = self.private_api.run_js_sync(code)
if callback is not None:
callback(result)

View File

@ -334,8 +334,8 @@ class Command:
Args:
param: The inspect.Parameter to look at.
"""
arginfo = self.get_arg_info(param)
if arginfo.value:
arg_info = self.get_arg_info(param)
if arg_info.value:
# Filled values are passed 1:1
return None
elif param.kind in [inspect.Parameter.VAR_POSITIONAL,

View File

@ -259,9 +259,10 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
self._filepath = tempfile.mktemp(prefix='qutebrowser-userscript-',
dir=standarddir.runtime())
# pylint: disable=no-member,useless-suppression
os.mkfifo(self._filepath)
os.mkfifo(self._filepath, mode=0o600)
# pylint: enable=no-member,useless-suppression
except OSError as e:
self._filepath = None # Make sure it's not used
message.error("Error while creating FIFO: {}".format(e))
return

View File

@ -424,4 +424,8 @@ class CompletionView(QTreeView):
if not index.isValid():
raise cmdutils.CommandError("No item selected!")
text = self.model().data(index)
if not utils.supports_selection():
sel = False
utils.set_clipboard(text, selection=sel)

View File

@ -33,7 +33,7 @@ from qutebrowser.api import (cmdutils, hook, config, message, downloads,
interceptor, apitypes, qtutils)
logger = logging.getLogger('misc')
logger = logging.getLogger('network')
_host_blocker = typing.cast('HostBlocker', None)
@ -128,8 +128,8 @@ class HostBlocker:
"""Block the given request if necessary."""
if self._is_blocked(request_url=info.request_url,
first_party_url=info.first_party_url):
logger.info("Request to {} blocked by host blocker."
.format(info.request_url.host()))
logger.debug("Request to {} blocked by host blocker."
.format(info.request_url.host()))
info.block()
def _read_hosts_line(self, raw_line: bytes) -> typing.Set[str]:

View File

@ -185,9 +185,13 @@ def move_to_end_of_document(tab: apitypes.Tab) -> None:
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def toggle_selection(tab: apitypes.Tab) -> None:
"""Toggle caret selection mode."""
tab.caret.toggle_selection()
def toggle_selection(tab: apitypes.Tab, line: bool = False) -> None:
"""Toggle caret selection mode.
Args:
line: Enables line-selection.
"""
tab.caret.toggle_selection(line)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])

View File

@ -460,9 +460,9 @@ class ConfigCommands:
if filename is None:
filename = standarddir.config_py()
else:
filename = os.path.expanduser(filename)
if not os.path.isabs(filename):
filename = os.path.join(standarddir.config(), filename)
filename = os.path.expanduser(filename)
if os.path.exists(filename) and not force:
raise cmdutils.CommandError("{} already exists - use --force to "

View File

@ -365,6 +365,10 @@ content.cookies.accept:
desc: >-
Which cookies to accept.
With QtWebEngine, this setting also controls other features with tracking
capabilities similar to those of cookies; including IndexedDB, DOM storage,
filesystem API, service workers, and AppCache.
Note that with QtWebKit, only `all` and `never` are supported as per-domain
values. Setting `no-3rdparty` or `no-unknown-3rdparty` per-domain on
QtWebKit will have the same effect as `all`.
@ -440,7 +444,7 @@ content.developer_extras:
deleted: true
content.dns_prefetch:
default: false
default: true
type: Bool
backend:
QtWebKit: true
@ -558,10 +562,10 @@ content.headers.user_agent:
completions:
# See https://techblog.willshouse.com/2012/01/03/most-common-user-agents/
- - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/80.0.3987.163 Safari/537.36"
like Gecko) Chrome/81.0.4044.129 Safari/537.36"
- Chrome 80 Win10
- - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/80.0.3987.149 Safari/537.36 "
Gecko) Chrome/81.0.4044.138 Safari/537.36"
- Chrome 80 Linux
supports_pattern: true
desc: |
@ -581,7 +585,9 @@ content.headers.user_agent:
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:
default: true
@ -1375,6 +1381,19 @@ input.links_included_in_focus_chain:
supports_pattern: true
desc: Include hyperlinks in the keyboard focus chain when tabbing.
input.mouse.back_forward_buttons:
default: true
type: Bool
desc: Enable back and forward buttons on the mouse.
input.mouse.rocker_gestures:
default: false
type: Bool
desc: >-
Enable Opera-like mouse rocker gestures.
This disables the context menu.
input.partial_timeout:
default: 5000
type:
@ -1388,12 +1407,7 @@ input.partial_timeout:
cleared after this time.
input.rocker_gestures:
default: false
type: Bool
desc: >-
Enable Opera-like mouse rocker gestures.
This disables the context menu.
renamed: input.mouse.rocker_gestures
input.spatial_navigation:
default: false
@ -1555,10 +1569,15 @@ spellcheck.languages:
## statusbar
statusbar.hide:
type: Bool
default: false
desc: Hide the statusbar unless a message is shown.
statusbar.show:
default: always
type:
name: String
valid_values:
- always: Always show the statusbar.
- never: Always hide the statusbar.
- in-mode: Show the statusbar when in modes other than normal mode.
desc: When to show the statusbar.
statusbar.padding:
type: Padding
@ -1971,7 +1990,7 @@ url.searchengines:
The search engine named `DEFAULT` is used when `url.auto_search` is turned
on and something else than a URL was entered to be opened. Other search
engines can be used by prepending the search engine name to the search
term, e.g. `:open google qutebrowser`.
term, e.g. `:open google qutebrowser`.
url.start_pages:
type:
@ -2608,6 +2627,169 @@ colors.webpage.prefers_color_scheme_dark:
QtWebEngine: Qt 5.14
QtWebKit: false
## dark mode
colors.webpage.darkmode.enabled:
default: false
type: Bool
desc: >-
Render all web contents using a dark theme.
Example configurations from Chromium's `chrome://flags`:
- "With simple HSL/CIELAB/RGB-based inversion": Set
`colors.webpage.darkmode.algorithm` accordingly.
- "With selective image inversion": Set
`colors.webpage.darkmode.policy.images` to `smart`.
- "With selective inversion of non-image elements": Set
`colors.webpage.darkmode.threshold.text` to 150 and
`colors.webpage.darkmode.threshold.background` to 205.
- "With selective inversion of everything": Combines the two variants
above.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.algorithm:
default: lightness-cielab
desc: "Which algorithm to use for modifying how colors are rendered with
darkmode."
type:
name: String
valid_values:
- lightness-cielab: Modify colors by converting them to CIELAB color
space and inverting the L value.
- lightness-hsl: Modify colors by converting them to the HSL color space
and inverting the lightness (i.e. the "L" in HSL).
- brightness-rgb: Modify colors by subtracting each of r, g, and b from
their maximum value.
# kSimpleInvertForTesting is not exposed, as it's equivalent to
# kInvertBrightness without gamma correction, and only available for
# Chromium's automated tests
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.contrast:
default: 0.0
type:
name: Float
minval: -1.0
maxval: 1.0
desc: >-
Contrast for dark mode.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to
`lightness-hsl` or `brightness-rgb`.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.policy.images:
default: never
type:
name: String
valid_values:
- always: Apply dark mode filter to all images.
- never: Never apply dark mode filter to any images.
- smart: Apply dark mode based on image content.
desc: >-
Which images to apply dark mode to.
WARNING: On Qt 5.15.0, this setting can cause frequent renderer process
crashes due to a
https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt].
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.policy.page:
default: smart
type:
name: String
valid_values:
- always: Apply dark mode filter to all frames, regardless of content.
- smart: Apply dark mode filter to frames based on background color.
desc: Which pages to apply dark mode to.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.threshold.text:
default: 256
type:
name: Int
minval: 0
maxval: 256
desc: >-
Threshold for inverting text with dark mode.
Text colors with brightness below this threshold will be inverted, and
above it will be left as in the original, non-dark-mode page. Set to 256
to always invert text color or to 0 to never invert text color.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.threshold.background:
default: 0
type:
name: Int
minval: 0
maxval: 256
desc: >-
Threshold for inverting background elements with dark mode.
Background elements with brightness above this threshold will be inverted,
and below it will be left as in the original, non-dark-mode page. Set to
256 to never invert the color or to 0 to always invert it.
Note: This behavior is the opposite of
`colors.webpage.darkmode.threshold.text`!
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.grayscale.all:
default: false
type: Bool
desc: >-
Render all colors as grayscale.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to
`lightness-hsl` or `brightness-rgb`.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.grayscale.images:
default: 0.0
type:
name: Float
minval: 0.0
maxval: 1.0
desc: >-
Desaturation factor for images in dark mode.
If set to 0, images are left as-is. If set to 1, images are completely
grayscale. Values between 0 and 1 desaturate the colors accordingly.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
# emacs: '
## fonts
@ -2662,7 +2844,7 @@ fonts.contextmenu:
fonts.debug_console:
default: default_size default_family
type: QtFont
type: Font
desc: Font used for the debugging console.
fonts.downloads:
@ -2705,10 +2887,15 @@ fonts.statusbar:
type: Font
desc: Font used in the statusbar.
fonts.tabs:
fonts.tabs.selected:
default: default_size default_family
type: QtFont
desc: Font used in the tab bar.
type: Font
desc: Font used for selected tabs.
fonts.tabs.unselected:
default: default_size default_family
type: Font
desc: Font used for unselected tabs.
fonts.web.family.standard:
default: ''
@ -2799,6 +2986,7 @@ bindings.key_mappings:
<Ctrl-6>: <Ctrl-^>
<Ctrl-M>: <Return>
<Ctrl-J>: <Return>
<Ctrl-I>: <Tab>
<Shift-Return>: <Return>
<Enter>: <Return>
<Shift-Enter>: <Return>
@ -2896,6 +3084,7 @@ bindings.default:
N: search-prev
i: enter-mode insert
v: enter-mode caret
V: enter-mode caret ;; toggle-selection --line
"`": enter-mode set_mark
"'": enter-mode jump_mark
yy: yank
@ -2999,6 +3188,12 @@ bindings.default:
tIH: config-cycle -p -u *://*.{url:host}/* content.images ;; reload
tiu: config-cycle -p -t -u {url} content.images ;; reload
tIu: config-cycle -p -u {url} content.images ;; reload
tch: config-cycle -p -t -u *://{url:host}/* content.cookies.accept all no-3rdparty never ;; reload
tCh: config-cycle -p -u *://{url:host}/* content.cookies.accept all no-3rdparty never ;; reload
tcH: config-cycle -p -t -u *://*.{url:host}/* content.cookies.accept all no-3rdparty never ;; reload
tCH: config-cycle -p -u *://*.{url:host}/* content.cookies.accept all no-3rdparty never ;; reload
tcu: config-cycle -p -t -u {url} content.cookies.accept all no-3rdparty never ;; reload
tCu: config-cycle -p -u {url} content.cookies.accept all no-3rdparty never ;; reload
insert:
<Ctrl-E>: open-editor
<Shift-Ins>: insert-text -- {primary}
@ -3077,6 +3272,7 @@ bindings.default:
<Escape>: leave-mode
caret:
v: toggle-selection
V: toggle-selection --line
<Space>: toggle-selection
<Ctrl-Space>: drop-selection
c: enter-mode normal
@ -3214,3 +3410,18 @@ bindings.commands:
* register: Entered when qutebrowser is waiting for a register name/key for
commands like `:set-mark`.
## logging
logging.level.ram:
default: debug
type: LogLevel
desc:
Level for in-memory logs.
logging.level.console:
default: info
type: LogLevel
desc: >-
Level for console (stdout/stderr) logs.
Ignored if the `--loglevel` or `--debug` CLI flags are used.

View File

@ -332,6 +332,11 @@ class YamlMigrations(QObject):
new_name='tabs.mode_on_change',
true_value='persist',
false_value='normal')
self._migrate_renamed_bool(
old_name='statusbar.hide',
new_name='statusbar.show',
true_value='never',
false_value='always')
for setting in ['tabs.title.format',
'tabs.title.format_pinned',
@ -340,6 +345,10 @@ class YamlMigrations(QObject):
r'(?<!{)\{title\}(?!})',
r'{current_title}')
self._migrate_to_multiple('fonts.tabs',
('fonts.tabs.selected',
'fonts.tabs.unselected'))
# content.headers.user_agent can't be empty to get the default anymore.
setting = 'content.headers.user_agent'
self._migrate_none(setting, configdata.DATA[setting].default)
@ -446,6 +455,19 @@ class YamlMigrations(QObject):
self._settings[name][scope] = value
self.changed.emit()
def _migrate_to_multiple(self, old_name: str,
new_names: typing.Iterable[str]) -> None:
if old_name not in self._settings:
return
for new_name in new_names:
self._settings[new_name] = {}
for scope, val in self._settings[old_name].items():
self._settings[new_name][scope] = val
del self._settings[old_name]
self.changed.emit()
def _migrate_string_value(self, name: str,
source: str,
target: str) -> None:
@ -603,6 +625,17 @@ class ConfigPyWriter:
def _gen_header(self) -> typing.Iterator[str]:
"""Generate the initial header of the config."""
yield self._line("# Autogenerated config.py")
yield self._line("#")
note = ("NOTE: config.py is intended for advanced users who are "
"comfortable with manually migrating the config file on "
"qutebrowser upgrades. If you prefer, you can also configure "
"qutebrowser using the :set/:bind/:config-* commands without "
"having to write a config.py file.")
for line in textwrap.wrap(note):
yield self._line("# {}".format(line))
yield self._line("#")
yield self._line("# Documentation:")
yield self._line("# qute://help/configuring.html")
yield self._line("# qute://help/settings.html")

View File

@ -199,6 +199,90 @@ def qt_args(namespace: argparse.Namespace) -> typing.List[str]:
return argv
def _darkmode_settings() -> typing.Iterator[typing.Tuple[str, str]]:
"""Get necessary blink settings to configure dark mode for QtWebEngine."""
if not config.val.colors.webpage.darkmode.enabled:
return
# Mapping from a colors.webpage.darkmode.algorithm setting value to
# Chromium's DarkModeInversionAlgorithm enum values.
algorithms = {
# 0: kOff (not exposed)
# 1: kSimpleInvertForTesting (not exposed)
'brightness-rgb': 2, # kInvertBrightness
'lightness-hsl': 3, # kInvertLightness
'lightness-cielab': 4, # kInvertLightnessLAB
}
# Mapping from a colors.webpage.darkmode.policy.images setting value to
# Chromium's DarkModeImagePolicy enum values.
image_policies = {
'always': 0, # kFilterAll
'never': 1, # kFilterNone
'smart': 2, # kFilterSmart
}
# Mapping from a colors.webpage.darkmode.policy.page setting value to
# Chromium's DarkModePagePolicy enum values.
page_policies = {
'always': 0, # kFilterAll
'smart': 1, # kFilterByBackground
}
bools = {
True: 'true',
False: 'false',
}
_setting_description_type = typing.Tuple[
str, # qutebrowser option name
str, # darkmode setting name
# Mapping from the config value to a string (or something convertable
# to a string) which gets passed to Chromium.
typing.Optional[typing.Mapping[typing.Any, typing.Union[str, int]]],
]
if qtutils.version_check('5.15', compiled=False):
settings = [
('enabled', 'Enabled', bools),
('algorithm', 'InversionAlgorithm', algorithms),
] # type: typing.List[_setting_description_type]
mandatory_setting = 'enabled'
else:
settings = [
('algorithm', '', algorithms),
]
mandatory_setting = 'algorithm'
settings += [
('contrast', 'Contrast', None),
('policy.images', 'ImagePolicy', image_policies),
('policy.page', 'PagePolicy', page_policies),
('threshold.text', 'TextBrightnessThreshold', None),
('threshold.background', 'BackgroundBrightnessThreshold', None),
('grayscale.all', 'Grayscale', bools),
('grayscale.images', 'ImageGrayscale', None),
]
for setting, key, mapping in settings:
# To avoid blowing up the commandline length, we only pass modified
# settings to Chromium, as our defaults line up with Chromium's.
# However, we always pass enabled/algorithm to make sure dark mode gets
# actually turned on.
value = config.instance.get(
'colors.webpage.darkmode.' + setting,
fallback=setting == mandatory_setting)
if isinstance(value, usertypes.Unset):
continue
if mapping is not None:
value = mapping[value]
# FIXME: This is "forceDarkMode" starting with Chromium 83
prefix = 'darkMode'
yield prefix + key, str(value)
def _qtwebengine_args(namespace: argparse.Namespace) -> typing.Iterator[str]:
"""Get the QtWebEngine arguments to use based on the config."""
if not qtutils.version_check('5.11', compiled=False):
@ -224,6 +308,11 @@ def _qtwebengine_args(namespace: argparse.Namespace) -> typing.Iterator[str]:
yield '--enable-logging'
yield '--v=1'
blink_settings = list(_darkmode_settings())
if blink_settings:
yield '--blink-settings=' + ','.join('{}={}'.format(k, v)
for k, v in blink_settings)
settings = {
'qt.force_software_rendering': {
'software-opengl': None,

View File

@ -56,14 +56,14 @@ import typing
import attr
import yaml
from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtGui import QColor, QFont, QFontDatabase
from PyQt5.QtGui import QColor, QFontDatabase
from PyQt5.QtWidgets import QTabWidget, QTabBar, QApplication
from PyQt5.QtNetwork import QNetworkProxy
from qutebrowser.misc import objects, debugcachestats
from qutebrowser.config import configexc, configutils
from qutebrowser.utils import (standarddir, utils, qtutils, urlutils, urlmatch,
usertypes)
usertypes, log)
from qutebrowser.keyinput import keyutils
from qutebrowser.browser.network import pac
@ -82,7 +82,8 @@ BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
_Completions = typing.Optional[typing.Iterable[typing.Tuple[str, str]]]
_StrUnset = typing.Union[str, usertypes.Unset]
_StrUnsetNone = typing.Union[None, str, usertypes.Unset]
_UnsetNone = typing.Union[None, usertypes.Unset]
_StrUnsetNone = typing.Union[str, _UnsetNone]
class ValidValues:
@ -797,11 +798,14 @@ class _Numeric(BaseType): # pylint: disable=abstract-method
assert isinstance(bound, (int, float)), bound
return bound
def _validate_bounds(self, value: typing.Union[None, int, float],
def _validate_bounds(self,
value: typing.Union[int, float, _UnsetNone],
suffix: str = '') -> None:
"""Validate self.minval and self.maxval."""
if value is None:
return
elif isinstance(value, usertypes.Unset):
return
elif self.minval is not None and value < self.minval:
raise configexc.ValidationError(
value, "must be {}{} or bigger!".format(self.minval, suffix))
@ -837,7 +841,10 @@ class Int(_Numeric):
self.to_py(intval)
return intval
def to_py(self, value: typing.Optional[int]) -> typing.Optional[int]:
def to_py(
self,
value: typing.Union[int, _UnsetNone]
) -> typing.Union[int, _UnsetNone]:
self._basic_py_validation(value, int)
self._validate_bounds(value)
return value
@ -861,8 +868,8 @@ class Float(_Numeric):
def to_py(
self,
value: typing.Union[None, int, float],
) -> typing.Union[None, int, float]:
value: typing.Union[int, float, _UnsetNone],
) -> typing.Union[int, float, _UnsetNone]:
self._basic_py_validation(value, (int, float))
self._validate_bounds(value)
return value
@ -874,8 +881,8 @@ class Perc(_Numeric):
def to_py(
self,
value: typing.Union[None, float, int, str, usertypes.Unset]
) -> typing.Union[None, float, int, usertypes.Unset]:
value: typing.Union[float, int, str, _UnsetNone]
) -> typing.Union[float, int, _UnsetNone]:
self._basic_py_validation(value, (float, int, str))
if isinstance(value, usertypes.Unset):
return value
@ -1070,8 +1077,7 @@ class QtColor(BaseType):
except ValueError:
raise configexc.ValidationError(val, "must be a valid color value")
def to_py(self, value: _StrUnset) -> typing.Union[usertypes.Unset,
None, QColor]:
def to_py(self, value: _StrUnset) -> typing.Union[_UnsetNone, QColor]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
@ -1151,7 +1157,7 @@ class QssColor(BaseType):
class FontBase(BaseType):
"""Base class for Font/QtFont/FontFamily."""
"""Base class for Font/FontFamily."""
# Gets set when the config is initialized.
default_family = None # type: str
@ -1286,97 +1292,6 @@ class FontFamily(FontBase):
return value
class QtFont(FontBase):
"""A Font which gets converted to a QFont."""
__doc__ = Font.__doc__ # for src2asciidoc.py
def _parse_families(self, family_str: str) -> configutils.FontFamilies:
if family_str == 'default_family' and self.default_family is not None:
family_str = self.default_family
return configutils.FontFamilies.from_str(family_str)
def _set_style(self, font: QFont, match: typing.Match) -> None:
style = match.group('style')
style_map = {
'normal': QFont.StyleNormal,
'italic': QFont.StyleItalic,
'oblique': QFont.StyleOblique,
}
if style:
font.setStyle(style_map[style])
else:
font.setStyle(QFont.StyleNormal)
def _set_weight(self, font: QFont, match: typing.Match) -> None:
weight = match.group('weight')
namedweight = match.group('namedweight')
weight_map = {
'normal': QFont.Normal,
'bold': QFont.Bold,
}
if namedweight:
font.setWeight(weight_map[namedweight])
elif weight:
# based on qcssparser.cpp:setFontWeightFromValue
font.setWeight(min(int(weight) // 8, 99))
else:
font.setWeight(QFont.Normal)
def _set_size(self, font: QFont, match: typing.Match) -> None:
size = match.group('size')
if size:
if size == 'default_size':
size = self.default_size
if size is None:
# initial validation before default_size is set up.
pass
elif size.lower().endswith('pt'):
font.setPointSizeF(float(size[:-2]))
elif size.lower().endswith('px'):
font.setPixelSize(int(size[:-2]))
else:
# This should never happen as the regex only lets pt/px
# through.
raise ValueError("Unexpected size unit in {!r}!".format(
size)) # pragma: no cover
def _set_families(self, font: QFont, match: typing.Match) -> None:
family_str = match.group('family')
families = self._parse_families(family_str)
if hasattr(font, 'setFamilies'):
# Added in Qt 5.13
font.setFamily(families.family) # type: ignore[arg-type]
font.setFamilies(list(families))
else: # pragma: no cover
font.setFamily(families.to_str(quote=False))
def to_py(self, value: _StrUnset) -> typing.Union[usertypes.Unset,
None, QFont]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
match = self.font_regex.fullmatch(value)
if not match: # pragma: no cover
# This should never happen, as the regex always matches everything
# as family.
raise configexc.ValidationError(value, "must be a valid font")
font = QFont()
self._set_style(font, match)
self._set_weight(font, match)
self._set_size(font, match)
self._set_families(font, match)
return font
class Regex(BaseType):
"""A regular expression.
@ -1434,7 +1349,7 @@ class Regex(BaseType):
def to_py(
self,
value: typing.Union[str, typing.Pattern[str], usertypes.Unset]
) -> typing.Union[usertypes.Unset, None, typing.Pattern[str]]:
) -> typing.Union[_UnsetNone, typing.Pattern[str]]:
"""Get a compiled regex from either a string or a regex object."""
self._basic_py_validation(value, (str, self._regex_type))
if isinstance(value, usertypes.Unset):
@ -1525,7 +1440,7 @@ class Dict(BaseType):
def to_py(
self,
value: typing.Union[typing.Dict, usertypes.Unset, None]
value: typing.Union[typing.Dict, _UnsetNone]
) -> typing.Union[typing.Dict, usertypes.Unset]:
self._basic_py_validation(value, dict)
if isinstance(value, usertypes.Unset):
@ -1724,8 +1639,7 @@ class Proxy(BaseType):
def to_py(
self,
value: _StrUnset
) -> typing.Union[usertypes.Unset, None,
QNetworkProxy, _SystemProxy, pac.PACFetcher]:
) -> typing.Union[_UnsetNone, QNetworkProxy, _SystemProxy, pac.PACFetcher]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
@ -1795,10 +1709,7 @@ class FuzzyUrl(BaseType):
"""A URL which gets interpreted as search if needed."""
def to_py(
self,
value: _StrUnset
) -> typing.Union[None, QUrl, usertypes.Unset]:
def to_py(self, value: _StrUnset) -> typing.Union[QUrl, _UnsetNone]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
@ -1836,7 +1747,7 @@ class Padding(Dict):
def to_py( # type: ignore[override]
self,
value: typing.Union[usertypes.Unset, typing.Dict, None],
value: typing.Union[typing.Dict, _UnsetNone],
) -> typing.Union[usertypes.Unset, PaddingValues]:
d = super().to_py(value)
if isinstance(d, usertypes.Unset):
@ -1908,10 +1819,7 @@ class Url(BaseType):
"""A URL as a string."""
def to_py(
self,
value: _StrUnset
) -> typing.Union[usertypes.Unset, None, QUrl]:
def to_py(self, value: _StrUnset) -> typing.Union[_UnsetNone, QUrl]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
@ -2014,6 +1922,16 @@ class NewTabPosition(String):
('last', "At the end."))
class LogLevel(String):
"""A logging level."""
def __init__(self, none_ok: bool = False) -> None:
super().__init__(none_ok=none_ok)
self.valid_values = ValidValues(*[level.lower()
for level in log.LOG_LEVELS])
class Key(BaseType):
"""A name of a key."""
@ -2025,7 +1943,7 @@ class Key(BaseType):
def to_py(
self,
value: _StrUnset
) -> typing.Union[usertypes.Unset, None, keyutils.KeySequence]:
) -> typing.Union[_UnsetNone, keyutils.KeySequence]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
@ -2049,7 +1967,7 @@ class UrlPattern(BaseType):
def to_py(
self,
value: _StrUnset
) -> typing.Union[usertypes.Unset, None, urlmatch.UrlPattern]:
) -> typing.Union[_UnsetNone, urlmatch.UrlPattern]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value

View File

@ -9,12 +9,13 @@ qute://warning/sessions</span> to show it again at a later time.</span>
<p>Since Qt doesn't provide an API to load the history of a tab, qutebrowser relies on a reverse-engineered binary serialization format to load tab history from session files. With Qt 5.15, unfortunately that format changed (due to the underlying Chromium upgrade), in a way which makes it impossible for qutebrowser to load tab history from existing session data.</p>
<p>At the time of writing (April 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and will be released with qutebrowser v1.12.0.</p>
<p>At the time of writing (April 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and is expected to be released with qutebrowser v1.13.0.</p>
<p>As a stop-gap measure:</p>
<ul>
<li>Loading a session with this release will <b>only load the most recently opened page</b> for every tab. As a result, the back/forward-history of every tab <b>will be lost</b> as soon as the session is saved again.</li>
<li>Due to that, the <span class="mono">session.lazy_restore</span> setting does not have any effect.</li>
<li>A one-time backup of the session folder has been created at <span class="mono">{{ datadir }}{{ sep }}sessions{{ sep }}before-qt-515</span>.</li>
</ul>

View File

@ -62,3 +62,4 @@ rules:
max-params: "off"
prefer-named-capture-group: "off"
function-call-argument-newline: "off"
no-negated-condition: "off"

View File

@ -705,6 +705,18 @@ window._qutebrowser.caret = (function() {
*/
CaretBrowsing.isCaretVisible = false;
/**
* Selection modes.
* NOTE: Values need to line up with SelectionState in browsertab.py!
*
* @type {enum}
*/
CaretBrowsing.SelectionState = {
"NONE": "none",
"NORMAL": "normal",
"LINE": "line",
};
/**
* The actual caret element, an absolute-positioned flashing line.
* @type {Element}
@ -887,7 +899,11 @@ window._qutebrowser.caret = (function() {
CaretBrowsing.injectCaretStyles();
CaretBrowsing.toggle();
CaretBrowsing.initiated = true;
CaretBrowsing.selectionEnabled = selectionRange > 0;
if (selectionRange > 0) {
CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NORMAL;
} else {
CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NONE;
}
};
/**
@ -1145,16 +1161,45 @@ window._qutebrowser.caret = (function() {
}
};
CaretBrowsing.reverseSelection = () => {
const sel = window.getSelection();
sel.setBaseAndExtent(
sel.extentNode, sel.extentOffset, sel.baseNode,
sel.baseOffset
);
};
CaretBrowsing.selectLine = function() {
const sel = window.getSelection();
sel.modify("extend", "right", "lineboundary");
CaretBrowsing.reverseSelection();
sel.modify("extend", "left", "lineboundary");
CaretBrowsing.reverseSelection();
};
CaretBrowsing.updateLineSelection = function(direction, granularity) {
if (granularity !== "character" && granularity !== "word") {
window.
getSelection().
modify("extend", direction, granularity);
CaretBrowsing.selectLine();
}
};
CaretBrowsing.move = function(direction, granularity, count = 1) {
let action = "move";
if (CaretBrowsing.selectionEnabled) {
if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) {
action = "extend";
}
for (let i = 0; i < count; i++) {
window.
getSelection().
modify(action, direction, granularity);
if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) {
CaretBrowsing.updateLineSelection(direction, granularity);
} else {
window.
getSelection().
modify(action, direction, granularity);
}
}
if (CaretBrowsing.isWindows &&
@ -1174,7 +1219,7 @@ window._qutebrowser.caret = (function() {
CaretBrowsing.moveToBlock = function(paragraph, boundary, count = 1) {
let action = "move";
if (CaretBrowsing.selectionEnabled) {
if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) {
action = "extend";
}
for (let i = 0; i < count; i++) {
@ -1185,6 +1230,10 @@ window._qutebrowser.caret = (function() {
window.
getSelection().
modify(action, boundary, "paragraphboundary");
if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) {
CaretBrowsing.selectLine();
}
}
};
@ -1294,14 +1343,14 @@ window._qutebrowser.caret = (function() {
funcs.setInitialCursor = () => {
if (!CaretBrowsing.initiated) {
CaretBrowsing.setInitialCursor();
return CaretBrowsing.selectionEnabled;
return CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE;
}
if (window.getSelection().toString().length === 0) {
positionCaret();
}
CaretBrowsing.toggle();
return CaretBrowsing.selectionEnabled;
return CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE;
};
funcs.setFlags = (flags) => {
@ -1399,17 +1448,22 @@ window._qutebrowser.caret = (function() {
funcs.getSelection = () => window.getSelection().toString();
funcs.toggleSelection = () => {
CaretBrowsing.selectionEnabled = !CaretBrowsing.selectionEnabled;
return CaretBrowsing.selectionEnabled;
funcs.toggleSelection = (line) => {
if (line) {
CaretBrowsing.selectionState =
CaretBrowsing.SelectionState.LINE;
CaretBrowsing.selectLine();
CaretBrowsing.finishMove();
} else if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NORMAL) {
CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NORMAL;
} else {
CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NONE;
}
return CaretBrowsing.selectionState;
};
funcs.reverseSelection = () => {
const sel = window.getSelection();
sel.setBaseAndExtent(
sel.extentNode, sel.extentOffset, sel.baseNode,
sel.baseOffset
);
CaretBrowsing.reverseSelection();
};
return funcs;

View File

@ -0,0 +1,9 @@
// ==UserScript==
// @include https://www.reddit.com/*
// @include https://open.spotify.com/*
// ==/UserScript==
// Polyfill for a failing globalThis with older Qt versions.
"use strict";
window.globalThis = window;

View File

@ -96,6 +96,24 @@ class BindingTrie:
return utils.get_repr(self, children=self.children,
command=self.command)
def __str__(self) -> str:
return '\n'.join(self.string_lines(blank=True))
def string_lines(self, indent: int = 0,
blank: bool = False) -> typing.Sequence[str]:
"""Get a list of strings for a pretty-printed version of this trie."""
lines = []
if self.command is not None:
lines.append('{}=> {}'.format(' ' * indent, self.command))
for key, child in sorted(self.children.items()):
lines.append('{}{}:'.format(' ' * indent, key))
lines.extend(child.string_lines(indent=indent+1))
if blank:
lines.append('')
return lines
def update(self, mapping: typing.Mapping) -> None:
"""Add data from the given mapping to the trie."""
for key in mapping:
@ -140,23 +158,16 @@ class BaseKeyParser(QObject):
Not intended to be instantiated directly. Subclasses have to override
execute() to do whatever they want to.
Class Attributes:
Match: types of a match between a binding and the keystring.
partial: No keychain matched yet, but it's still possible in the
future.
definitive: Keychain matches exactly.
none: No more matches possible.
do_log: Whether to log keypresses or not.
passthrough: Whether unbound keys should be passed through with this
handler.
supports_count: Whether count is supported.
Attributes:
mode_name: The name of the mode in the config.
bindings: Bound key bindings
_mode: The usertypes.KeyMode associated with this keyparser.
_win_id: The window ID this keyparser is associated with.
_sequence: The currently entered key sequence
_modename: The name of the input mode associated with this keyparser.
_do_log: Whether to log keypresses or not.
passthrough: Whether unbound keys should be passed through with this
handler.
_supports_count: Whether count is supported.
Signals:
keystring_updated: Emitted when the keystring is updated.
@ -169,21 +180,31 @@ class BaseKeyParser(QObject):
keystring_updated = pyqtSignal(str)
request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
do_log = True
passthrough = False
supports_count = True
def __init__(self, win_id: int, parent: QObject = None) -> None:
def __init__(self, *, mode: usertypes.KeyMode,
win_id: int,
parent: QObject = None,
do_log: bool = True,
passthrough: bool = False,
supports_count: bool = True) -> None:
super().__init__(parent)
self._win_id = win_id
self._modename = None
self._sequence = keyutils.KeySequence()
self._count = ''
self._mode = mode
self._do_log = do_log
self.passthrough = passthrough
self._supports_count = supports_count
self.bindings = BindingTrie()
self._read_config()
config.instance.changed.connect(self._on_config_changed)
def __repr__(self) -> str:
return utils.get_repr(self)
return utils.get_repr(self, mode=self._mode,
win_id=self._win_id,
do_log=self._do_log,
passthrough=self.passthrough,
supports_count=self._supports_count)
def _debug_log(self, message: str) -> None:
"""Log a message to the debug log if logging is active.
@ -191,8 +212,10 @@ class BaseKeyParser(QObject):
Args:
message: The message to log.
"""
if self.do_log:
log.keyboard.debug(message)
if self._do_log:
prefix = '{} for mode {}: '.format(self.__class__.__name__,
self._mode.name)
log.keyboard.debug(prefix + message)
def _match_key(self, sequence: keyutils.KeySequence) -> MatchResult:
"""Try to match a given keystring with any bound keychain.
@ -234,7 +257,7 @@ class BaseKeyParser(QObject):
dry_run: bool) -> bool:
"""Try to match a key as count."""
txt = str(sequence[-1]) # To account for sequences changed above.
if (txt in string.digits and self.supports_count and
if (txt in string.digits and self._supports_count and
not (not self._count and txt == '0')):
self._debug_log("Trying match as count")
assert len(txt) == 1, txt
@ -319,25 +342,12 @@ class BaseKeyParser(QObject):
def _on_config_changed(self) -> None:
self._read_config()
def _read_config(self, modename: str = None) -> None:
"""Read the configuration.
Config format: key = command, e.g.:
<Ctrl+Q> = quit
Args:
modename: Name of the mode to use.
"""
if modename is None:
if self._modename is None:
raise ValueError("read_config called with no mode given, but "
"None defined so far!")
modename = self._modename
else:
self._modename = modename
def _read_config(self) -> None:
"""Read the configuration."""
self.bindings = BindingTrie()
config_bindings = config.key_instance.get_bindings_for(self._mode.name)
for key, cmd in config.key_instance.get_bindings_for(modename).items():
for key, cmd in config_bindings.items():
assert cmd
self.bindings[key] = cmd

View File

@ -180,21 +180,6 @@ def _is_printable(key: Qt.Key) -> bool:
return key <= 0xff and key not in [Qt.Key_Space, _NIL_KEY]
def is_special_hint_mode(key: Qt.Key, modifiers: _ModifierType) -> bool:
"""Check whether this key should clear the keychain in hint mode.
When we press "s<Escape>", we don't want <Escape> to be handled as part of
a key chain in hint mode.
"""
_assert_plain_key(key)
_assert_plain_modifier(modifiers)
if is_modifier_key(key):
return False
return not (_is_printable(key) and
modifiers in [Qt.ShiftModifier, Qt.NoModifier,
Qt.KeypadModifier])
def is_special(key: Qt.Key, modifiers: _ModifierType) -> bool:
"""Check whether this key requires special key syntax."""
_assert_plain_key(key)

View File

@ -68,6 +68,14 @@ class NotInModeError(Exception):
"""Exception raised when we want to leave a mode we're not in."""
class UnavailableError(Exception):
"""Exception raised when trying to access modeman before initialization.
Thrown by instance() if modeman has not been initialized yet.
"""
def init(win_id: int, parent: QObject) -> 'ModeManager':
"""Initialize the mode manager and the keyparsers for the given win_id."""
modeman = ModeManager(win_id, parent)
@ -94,70 +102,86 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
parent=modeman),
usertypes.KeyMode.insert:
modeparsers.PassthroughKeyParser(
win_id=win_id,
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.insert,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
parent=modeman,
passthrough=True,
do_log=False,
supports_count=False),
usertypes.KeyMode.passthrough:
modeparsers.PassthroughKeyParser(
win_id=win_id,
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.passthrough,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
parent=modeman,
passthrough=True,
do_log=False,
supports_count=False),
usertypes.KeyMode.command:
modeparsers.PassthroughKeyParser(
win_id=win_id,
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.command,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
parent=modeman,
passthrough=True,
do_log=False,
supports_count=False),
usertypes.KeyMode.prompt:
modeparsers.PassthroughKeyParser(
win_id=win_id,
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.prompt,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
parent=modeman,
passthrough=True,
do_log=False,
supports_count=False),
usertypes.KeyMode.yesno:
modeparsers.PromptKeyParser(
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.yesno,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
parent=modeman,
supports_count=False),
usertypes.KeyMode.caret:
modeparsers.CaretKeyParser(
modeparsers.CommandKeyParser(
mode=usertypes.KeyMode.caret,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
parent=modeman,
passthrough=True),
usertypes.KeyMode.set_mark:
modeparsers.RegisterKeyParser(
win_id=win_id,
mode=usertypes.KeyMode.set_mark,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
usertypes.KeyMode.jump_mark:
modeparsers.RegisterKeyParser(
win_id=win_id,
mode=usertypes.KeyMode.jump_mark,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
usertypes.KeyMode.record_macro:
modeparsers.RegisterKeyParser(
win_id=win_id,
mode=usertypes.KeyMode.record_macro,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
usertypes.KeyMode.run_macro:
modeparsers.RegisterKeyParser(
win_id=win_id,
mode=usertypes.KeyMode.run_macro,
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
} # type: ParserDictType
@ -169,8 +193,16 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
def instance(win_id: Union[int, str]) -> 'ModeManager':
"""Get a modemanager object."""
return objreg.get('mode-manager', scope='window', window=win_id)
"""Get a modemanager object.
Raises UnavailableError if there is no instance available yet.
"""
mode_manager = objreg.get('mode-manager', scope='window', window=win_id,
default=None)
if mode_manager is not None:
return mode_manager
else:
raise UnavailableError("ModeManager is not initialized yet.")
def enter(win_id: int,
@ -211,10 +243,15 @@ class ModeManager(QObject):
arg1: The mode which has been left.
arg2: The new current mode.
arg3: The window ID of this mode manager.
keystring_updated: Emitted when the keystring was updated in any mode.
arg 1: The mode in which the keystring has been
updated.
arg 2: The new key string.
"""
entered = pyqtSignal(usertypes.KeyMode, int)
left = pyqtSignal(usertypes.KeyMode, usertypes.KeyMode, int)
keystring_updated = pyqtSignal(usertypes.KeyMode, str)
def __init__(self, win_id: int, parent: QObject = None) -> None:
super().__init__(parent)
@ -300,6 +337,8 @@ class ModeManager(QObject):
assert parser is not None
self.parsers[mode] = parser
parser.request_leave.connect(self.leave)
parser.keystring_updated.connect(
functools.partial(self.keystring_updated.emit, mode))
def enter(self, mode: usertypes.KeyMode,
reason: str = None,

View File

@ -51,10 +51,16 @@ class CommandKeyParser(basekeyparser.BaseKeyParser):
_commandrunner: CommandRunner instance.
"""
def __init__(self, win_id: int,
def __init__(self, *, mode: usertypes.KeyMode,
win_id: int,
commandrunner: 'runners.CommandRunner',
parent: QObject = None) -> None:
super().__init__(win_id, parent)
parent: QObject = None,
do_log: bool = True,
passthrough: bool = False,
supports_count: bool = True) -> None:
super().__init__(mode=mode, win_id=win_id, parent=parent,
do_log=do_log, passthrough=passthrough,
supports_count=supports_count)
self._commandrunner = commandrunner
def execute(self, cmdstr: str, count: int = None) -> None:
@ -72,11 +78,11 @@ class NormalKeyParser(CommandKeyParser):
_partial_timer: Timer to clear partial keypresses.
"""
def __init__(self, win_id: int,
def __init__(self, *, win_id: int,
commandrunner: 'runners.CommandRunner',
parent: QObject = None) -> None:
super().__init__(win_id, commandrunner, parent)
self._read_config('normal')
super().__init__(mode=usertypes.KeyMode.normal, win_id=win_id,
commandrunner=commandrunner, parent=parent)
self._partial_timer = usertypes.Timer(self, 'partial-match')
self._partial_timer.setSingleShot(True)
self._partial_timer.timeout.connect(self._clear_partial_match)
@ -130,56 +136,7 @@ class NormalKeyParser(CommandKeyParser):
self._inhibited = False
class PassthroughKeyParser(CommandKeyParser):
"""KeyChainParser which passes through normal keys.
Used for insert/passthrough modes.
Attributes:
_mode: The mode this keyparser is for.
"""
do_log = False
passthrough = True
supports_count = False
def __init__(self, win_id: int,
mode: usertypes.KeyMode,
commandrunner: 'runners.CommandRunner',
parent: QObject = None) -> None:
"""Constructor.
Args:
mode: The mode this keyparser is for.
parent: Qt parent.
warn: Whether to warn if an ignored key was bound.
"""
super().__init__(win_id, commandrunner, parent)
self._read_config(mode.name)
self._mode = mode
def __repr__(self) -> str:
return utils.get_repr(self, mode=self._mode)
class PromptKeyParser(CommandKeyParser):
"""KeyParser for yes/no prompts."""
supports_count = False
def __init__(self, win_id: int,
commandrunner: 'runners.CommandRunner',
parent: QObject = None) -> None:
super().__init__(win_id, commandrunner, parent)
self._read_config('yesno')
def __repr__(self) -> str:
return utils.get_repr(self)
class HintKeyParser(CommandKeyParser):
class HintKeyParser(basekeyparser.BaseKeyParser):
"""KeyChainParser for hints.
@ -189,17 +146,20 @@ class HintKeyParser(CommandKeyParser):
_last_press: The nature of the last keypress, a LastPress member.
"""
supports_count = False
def __init__(self, win_id: int,
def __init__(self, *, win_id: int,
commandrunner: 'runners.CommandRunner',
hintmanager: hints.HintManager,
parent: QObject = None) -> None:
super().__init__(win_id, commandrunner, parent)
super().__init__(mode=usertypes.KeyMode.hint, win_id=win_id,
parent=parent, supports_count=False)
self._command_parser = CommandKeyParser(mode=usertypes.KeyMode.hint,
win_id=win_id,
commandrunner=commandrunner,
parent=self,
supports_count=False)
self._hintmanager = hintmanager
self._filtertext = ''
self._last_press = LastPress.none
self._read_config('hint')
self.keystring_updated.connect(self._hintmanager.handle_partial_key)
def _handle_filter_key(self, e: QKeyEvent) -> QKeySequence.SequenceMatch:
@ -242,11 +202,14 @@ class HintKeyParser(CommandKeyParser):
if dry_run:
return super().handle(e, dry_run=True)
if keyutils.is_special_hint_mode(Qt.Key(e.key()), e.modifiers()):
log.keyboard.debug("Got special key, clearing keychain")
self.clear_keystring()
assert not dry_run
if (self._command_parser.handle(e, dry_run=True) !=
QKeySequence.NoMatch):
log.keyboard.debug("Handling key via command parser")
self.clear_keystring()
return self._command_parser.handle(e)
match = super().handle(e)
if match == QKeySequence.PartialMatch:
@ -271,23 +234,14 @@ class HintKeyParser(CommandKeyParser):
`self._filtertext`.
"""
self._read_config()
self.bindings.update({keyutils.KeySequence.parse(s):
'follow-hint -s ' + s for s in strings})
self.bindings.update({keyutils.KeySequence.parse(s): s
for s in strings})
if not preserve_filter:
self._filtertext = ''
class CaretKeyParser(CommandKeyParser):
"""KeyParser for caret mode."""
passthrough = True
def __init__(self, win_id: int,
commandrunner: 'runners.CommandRunner',
parent: QObject = None) -> None:
super().__init__(win_id, commandrunner, parent)
self._read_config('caret')
def execute(self, cmdstr: str, count: int = None) -> None:
assert count is None
self._hintmanager.handle_partial_key(cmdstr)
class RegisterKeyParser(CommandKeyParser):
@ -295,19 +249,18 @@ class RegisterKeyParser(CommandKeyParser):
"""KeyParser for modes that record a register key.
Attributes:
_mode: One of KeyMode.set_mark, KeyMode.jump_mark, KeyMode.record_macro
and KeyMode.run_macro.
_register_mode: One of KeyMode.set_mark, KeyMode.jump_mark,
KeyMode.record_macro and KeyMode.run_macro.
"""
supports_count = False
def __init__(self, win_id: int,
def __init__(self, *, win_id: int,
mode: usertypes.KeyMode,
commandrunner: 'runners.CommandRunner',
parent: QObject = None) -> None:
super().__init__(win_id, commandrunner, parent)
self._mode = mode
self._read_config('register')
super().__init__(mode=usertypes.KeyMode.register, win_id=win_id,
commandrunner=commandrunner, parent=parent,
supports_count=False)
self._register_mode = mode
def handle(self, e: QKeyEvent, *,
dry_run: bool = False) -> QKeySequence.SequenceMatch:
@ -326,19 +279,20 @@ class RegisterKeyParser(CommandKeyParser):
window=self._win_id)
try:
if self._mode == usertypes.KeyMode.set_mark:
if self._register_mode == usertypes.KeyMode.set_mark:
tabbed_browser.set_mark(key)
elif self._mode == usertypes.KeyMode.jump_mark:
elif self._register_mode == usertypes.KeyMode.jump_mark:
tabbed_browser.jump_mark(key)
elif self._mode == usertypes.KeyMode.record_macro:
elif self._register_mode == usertypes.KeyMode.record_macro:
macros.macro_recorder.record_macro(key)
elif self._mode == usertypes.KeyMode.run_macro:
elif self._register_mode == usertypes.KeyMode.run_macro:
macros.macro_recorder.run_macro(self._win_id, key)
else:
raise ValueError(
"{} is not a valid register mode".format(self._mode))
raise ValueError("{} is not a valid register mode".format(
self._register_mode))
except cmdexc.Error as err:
message.error(str(err), stack=traceback.format_exc())
self.request_leave.emit(self._mode, "valid register key", True)
self.request_leave.emit(
self._register_mode, "valid register key", True)
return QKeySequence.ExactMatch

View File

@ -505,9 +505,8 @@ class MainWindow(QWidget):
message.global_bridge.mode_left) # type: ignore[arg-type]
# commands
normal_parser = mode_manager.parsers[usertypes.KeyMode.normal]
normal_parser.keystring_updated.connect(
self.status.keystring.setText)
mode_manager.keystring_updated.connect(
self.status.keystring.on_keystring_updated)
self.status.cmd.got_cmd[str].connect( # type: ignore[index]
self._commandrunner.run_safely)
self.status.cmd.got_cmd[str, int].connect( # type: ignore[index]
@ -518,9 +517,7 @@ class MainWindow(QWidget):
self._command_dispatcher.search)
# key hint popup
for mode, parser in mode_manager.parsers.items():
parser.keystring_updated.connect(functools.partial(
self._keyhint.update_keyhint, mode.name))
mode_manager.keystring_updated.connect(self._keyhint.update_keyhint)
# messages
message.global_bridge.show_message.connect(

View File

@ -203,7 +203,7 @@ class StatusBar(QWidget):
@pyqtSlot(str)
def _on_config_changed(self, option):
if option == 'statusbar.hide':
if option == 'statusbar.show':
self.maybe_hide()
elif option == 'statusbar.padding':
self._set_hbox_padding()
@ -254,12 +254,26 @@ class StatusBar(QWidget):
@pyqtSlot()
def maybe_hide(self):
"""Hide the statusbar if it's configured to do so."""
strategy = config.val.statusbar.show
tab = self._current_tab()
hide = config.val.statusbar.hide
if hide or (tab is not None and tab.data.fullscreen):
if tab is not None and tab.data.fullscreen:
self.hide()
else:
elif strategy == 'never':
self.hide()
elif strategy == 'in-mode':
try:
mode_manager = modeman.instance(self._win_id)
except modeman.UnavailableError:
self.hide()
else:
if mode_manager.mode == usertypes.KeyMode.normal:
self.hide()
else:
self.show()
elif strategy == 'always':
self.show()
else:
raise utils.Unreachable
def _set_hbox_padding(self):
padding = config.val.statusbar.padding
@ -336,6 +350,8 @@ class StatusBar(QWidget):
def on_mode_entered(self, mode):
"""Mark certain modes in the commandline."""
mode_manager = modeman.instance(self._win_id)
if config.val.statusbar.show == 'in-mode':
self.show()
if mode_manager.parsers[mode].passthrough:
self._set_mode_text(mode.name)
if mode in [usertypes.KeyMode.insert,
@ -350,6 +366,8 @@ class StatusBar(QWidget):
def on_mode_left(self, old_mode, new_mode):
"""Clear marked mode."""
mode_manager = modeman.instance(self._win_id)
if config.val.statusbar.show == 'in-mode':
self.hide()
if mode_manager.parsers[old_mode].passthrough:
if mode_manager.parsers[new_mode].passthrough:
self._set_mode_text(new_mode.name)
@ -373,13 +391,17 @@ class StatusBar(QWidget):
self.maybe_hide()
assert tab.is_private == self._color_flags.private
@pyqtSlot(bool)
def on_caret_selection_toggled(self, selection):
@pyqtSlot(browsertab.SelectionState)
def on_caret_selection_toggled(self, selection_state):
"""Update the statusbar when entering/leaving caret selection mode."""
log.statusbar.debug("Setting caret selection {}".format(selection))
if selection:
log.statusbar.debug("Setting caret selection {}"
.format(selection_state))
if selection_state is browsertab.SelectionState.normal:
self._set_mode_text("caret selection")
self._color_flags.caret = ColorFlags.CaretMode.selection
elif selection_state is browsertab.SelectionState.line:
self._set_mode_text("caret line selection")
self._color_flags.caret = ColorFlags.CaretMode.selection
else:
self._set_mode_text("caret")
self._color_flags.caret = ColorFlags.CaretMode.on

View File

@ -19,9 +19,16 @@
"""Keychain string displayed in the statusbar."""
from PyQt5.QtCore import pyqtSlot
from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.utils import usertypes
class KeyString(textbase.TextBase):
"""Keychain string displayed in the statusbar."""
@pyqtSlot(usertypes.KeyMode, str)
def on_keystring_updated(self, _mode, keystr):
self.setText(keystr)

View File

@ -64,8 +64,11 @@ class TabDeque:
"""
def __init__(self) -> None:
size = config.val.tabs.focus_stack_size
if size < 0:
size = None
self._stack = collections.deque(
maxlen=config.val.tabs.focus_stack_size
maxlen=size
) # type: typing.Deque[weakref.ReferenceType[QWidget]]
# Items that have been removed from the primary stack.
self._stack_deleted = [
@ -189,7 +192,7 @@ class TabbedBrowser(QWidget):
cur_scroll_perc_changed = pyqtSignal(int, int)
cur_load_status_changed = pyqtSignal(usertypes.LoadStatus)
cur_fullscreen_requested = pyqtSignal(bool)
cur_caret_selection_toggled = pyqtSignal(bool)
cur_caret_selection_toggled = pyqtSignal(browsertab.SelectionState)
close_window = pyqtSignal()
resized = pyqtSignal('QRect')
current_tab_changed = pyqtSignal(browsertab.AbstractTab)

View File

@ -385,8 +385,13 @@ class TabBar(QTabBar):
STYLESHEET = """
TabBar {
font: {{ conf.fonts.tabs.unselected }};
background-color: {{ conf.colors.tabs.bar.bg }};
}
TabBar::tab:selected {
font: {{ conf.fonts.tabs.selected }};
}
"""
new_tab_requested = pyqtSignal()
@ -395,8 +400,6 @@ class TabBar(QTabBar):
super().__init__(parent)
self._win_id = win_id
self.setStyle(TabBarStyle())
self._set_font()
config.instance.changed.connect(self._on_config_changed)
self.vertical = False
self._auto_hide_timer = QTimer()
self._auto_hide_timer.setSingleShot(True)
@ -405,6 +408,9 @@ class TabBar(QTabBar):
self.setAutoFillBackground(True)
self.drag_in_progress = False
stylesheet.set_register(self)
self.ensurePolished()
config.instance.changed.connect(self._on_config_changed)
self._set_icon_size()
QTimer.singleShot(0, self.maybe_hide)
def __repr__(self):
@ -416,8 +422,9 @@ class TabBar(QTabBar):
@pyqtSlot(str)
def _on_config_changed(self, option: str) -> None:
if option == 'fonts.tabs':
self._set_font()
if option.startswith('fonts.tabs.'):
self.ensurePolished()
self._set_icon_size()
elif option == 'tabs.favicons.scale':
self._set_icon_size()
elif option == 'tabs.show_switching_delay':
@ -433,7 +440,9 @@ class TabBar(QTabBar):
"tabs.padding",
"tabs.indicator.width",
"tabs.min_width",
"tabs.pinned.shrink"]:
"tabs.pinned.shrink",
"fonts.tabs.selected",
"fonts.tabs.unselected"]:
self._minimum_tab_size_hint_helper.cache_clear()
self._minimum_tab_height.cache_clear()
@ -506,14 +515,6 @@ class TabBar(QTabBar):
# code sets layoutDirty so it actually relayouts the tabs.
self.setIconSize(self.iconSize())
def _set_font(self):
"""Set the tab bar font."""
self.setFont(config.val.fonts.tabs)
self._set_icon_size()
# clear tab size cache
self._minimum_tab_size_hint_helper.cache_clear()
self._minimum_tab_height.cache_clear()
def _set_icon_size(self):
"""Set the tab bar favicon size."""
size = self.fontMetrics().height() - 2

View File

@ -23,8 +23,6 @@ import os
import sys
import functools
import html
import ctypes
import ctypes.util
import enum
import shutil
import typing
@ -59,7 +57,7 @@ class _Button:
text = attr.ib() # type: str
setting = attr.ib() # type: str
value = attr.ib() # type: str
value = attr.ib() # type: typing.Any
default = attr.ib(default=False) # type: bool
@ -81,19 +79,21 @@ def _error_text(because: str, text: str, backend: usertypes.Backend) -> str:
if other_backend == usertypes.Backend.QtWebKit:
warning = ("<i>Note that QtWebKit hasn't been updated since "
"July 2017 (including security updates).</i>")
suffix = " (not recommended)"
else:
warning = ""
suffix = ""
return ("<b>Failed to start with the {backend} backend!</b>"
"<p>qutebrowser tried to start with the {backend} backend but "
"failed because {because}.</p>{text}"
"<p><b>Forcing the {other_backend.name} backend</b></p>"
"<p><b>Forcing the {other_backend.name} backend{suffix}</b></p>"
"<p>This forces usage of the {other_backend.name} backend by "
"setting the <i>backend = '{other_setting}'</i> option "
"(if you have a <i>config.py</i> file, you'll need to set "
"this manually). {warning}</p>".format(
backend=backend.name, because=because, text=text,
other_backend=other_backend, other_setting=other_setting,
warning=warning))
warning=warning, suffix=suffix))
class _Dialog(QDialog):
@ -167,6 +167,14 @@ class _BackendProblemChecker:
"""Check for various backend-specific issues."""
SOFTWARE_RENDERING_TEXT = (
"<p><b>Forcing software rendering</b></p>"
"<p>This allows you to use the newer QtWebEngine backend (based on "
"Chromium) but could have noticeable performance impact (depending on "
"your hardware). This sets the <i>qt.force_software_rendering = "
"'chromium'</i> option (if you have a <i>config.py</i> file, you'll "
"need to set this manually).</p>")
def __init__(self, *,
no_err_windows: bool,
save_manager: savemanager.SaveManager) -> None:
@ -201,19 +209,10 @@ class _BackendProblemChecker:
def _nvidia_shader_workaround(self) -> None:
"""Work around QOpenGLShaderProgram issues.
NOTE: This needs to be called before _handle_nouveau_graphics, or some
setups will segfault in version.opengl_vendor().
See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
"""
self._assert_backend(usertypes.Backend.QtWebEngine)
if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'):
return
libgl = ctypes.util.find_library("GL")
if libgl is not None:
ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL)
utils.libgl_workaround()
def _handle_nouveau_graphics(self) -> None:
"""Force software rendering when using the Nouveau driver.
@ -231,7 +230,8 @@ class _BackendProblemChecker:
if qtutils.version_check('5.10', compiled=False):
return
if version.opengl_vendor() != 'nouveau':
opengl_info = version.opengl_info()
if opengl_info is None or opengl_info.vendor != 'nouveau':
return
if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
@ -248,36 +248,15 @@ class _BackendProblemChecker:
self._show_dialog(
backend=usertypes.Backend.QtWebEngine,
because="you're using Nouveau graphics",
text=("<p>There are two ways to fix this:</p>"
"<p><b>Forcing software rendering</b></p>"
"<p>This allows you to use the newer QtWebEngine backend "
"(based on Chromium) but could have noticeable performance "
"impact (depending on your hardware). This sets the "
"<i>qt.force_software_rendering = 'chromium'</i> option "
"(if you have a <i>config.py</i> file, you'll need to set "
"this manually).</p>"),
text=("<p>There are two ways to fix this:</p>" +
self.SOFTWARE_RENDERING_TEXT),
buttons=[button],
)
raise utils.Unreachable
def _handle_wayland(self) -> None:
self._assert_backend(usertypes.Backend.QtWebEngine)
if os.environ.get('QUTE_SKIP_WAYLAND_CHECK'):
return
platform = QApplication.instance().platformName()
if platform not in ['wayland', 'wayland-egl']:
return
has_qt511 = qtutils.version_check('5.11', compiled=False)
if has_qt511 and config.val.qt.force_software_rendering == 'chromium':
return
if qtutils.version_check('5.11.2', compiled=False):
return
def _xwayland_options(self) -> typing.Tuple[str, typing.List[_Button]]:
"""Get buttons/text for a possible XWayland solution."""
buttons = []
text = "<p>You can work around this in one of the following ways:</p>"
@ -296,23 +275,86 @@ class _BackendProblemChecker:
"<p>This allows you to use the newer QtWebEngine backend "
"(based on Chromium). ")
return text, buttons
def _handle_wayland(self) -> None:
self._assert_backend(usertypes.Backend.QtWebEngine)
if os.environ.get('QUTE_SKIP_WAYLAND_CHECK'):
return
platform = QApplication.instance().platformName()
if platform not in ['wayland', 'wayland-egl']:
return
has_qt511 = qtutils.version_check('5.11', compiled=False)
if has_qt511 and config.val.qt.force_software_rendering == 'chromium':
return
if qtutils.version_check('5.11.2', compiled=False):
return
text, buttons = self._xwayland_options()
if has_qt511:
buttons.append(_Button("Force software rendering",
'qt.force_software_rendering',
'chromium'))
text += ("<p><b>Forcing software rendering</b></p>"
"<p>This allows you to use the newer QtWebEngine backend "
"(based on Chromium) but could have noticeable "
"performance impact (depending on your hardware). This "
"sets the <i>qt.force_software_rendering = "
"'chromium'</i> option (if you have a <i>config.py</i> "
"file, you'll need to set this manually).</p>")
text += self.SOFTWARE_RENDERING_TEXT
self._show_dialog(backend=usertypes.Backend.QtWebEngine,
because="you're using Wayland",
text=text,
buttons=buttons)
def _handle_wayland_webgl(self) -> None:
"""On older graphic hardware, WebGL on Wayland causes segfaults.
See https://github.com/qutebrowser/qutebrowser/issues/5313
"""
self._assert_backend(usertypes.Backend.QtWebEngine)
if os.environ.get('QUTE_SKIP_WAYLAND_WEBGL_CHECK'):
return
platform = QApplication.instance().platformName()
if platform not in ['wayland', 'wayland-egl']:
return
# Only Qt 5.14 should be affected
if not qtutils.version_check('5.14', compiled=False):
return
if qtutils.version_check('5.15', compiled=False):
return
# Newer graphic hardware isn't affected
opengl_info = version.opengl_info()
if (opengl_info is None or
opengl_info.gles or
opengl_info.version is None or
opengl_info.version >= (4, 3)):
return
# If WebGL is turned off, we're fine
if not config.val.content.webgl:
return
text, buttons = self._xwayland_options()
buttons.append(_Button("Turn off WebGL (recommended)",
'content.webgl',
False))
text += ("<p><b>Disable WebGL (recommended)</b></p>"
"This sets the <i>content.webgl = False</i> option "
"(if you have a <i>config.py</i> file, you'll need to "
"set this manually).</p>")
self._show_dialog(backend=usertypes.Backend.QtWebEngine,
because=("of frequent crashes with Qt 5.14 on "
"Wayland with older graphics hardware"),
text=text,
buttons=buttons)
def _try_import_backends(self) -> _BackendImports:
"""Check whether backends can be imported and return BackendImports."""
# pylint: disable=unused-import
@ -320,8 +362,9 @@ class _BackendProblemChecker:
try:
from PyQt5 import QtWebKit
from PyQt5.QtWebKit import qWebKitVersion
from PyQt5 import QtWebKitWidgets
except ImportError as e:
except (ImportError, ValueError) as e:
results.webkit_available = False
results.webkit_error = str(e)
else:
@ -333,7 +376,7 @@ class _BackendProblemChecker:
try:
from PyQt5 import QtWebEngineWidgets
except ImportError as e:
except (ImportError, ValueError) as e:
results.webengine_available = False
results.webengine_error = str(e)
else:
@ -494,6 +537,7 @@ class _BackendProblemChecker:
self._handle_ssl_support()
self._handle_wayland()
self._nvidia_shader_workaround()
self._handle_wayland_webgl()
self._handle_nouveau_graphics()
self._handle_cache_nuking()
self._handle_serviceworker_nuking()

View File

@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt
from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QApplication
from PyQt5.QtGui import QTextCursor
from qutebrowser.config import config
from qutebrowser.config import stylesheet
from qutebrowser.misc import cmdhistory, miscwidgets
from qutebrowser.utils import utils, objreg
@ -55,8 +55,6 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
_namespace: The local namespace of the interpreter.
"""
super().__init__(parent=parent)
self._update_font()
config.instance.changed.connect(self._update_font)
self._history = cmdhistory.History(parent=self)
self.returnPressed.connect(self.on_return_pressed)
@ -106,11 +104,6 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
else:
super().keyPressEvent(e)
@config.change_filter('fonts.debug_console')
def _update_font(self):
"""Set the correct font."""
self.setFont(config.val.fonts.debug_console)
class ConsoleTextEdit(QTextEdit):
@ -120,18 +113,11 @@ class ConsoleTextEdit(QTextEdit):
super().__init__(parent)
self.setAcceptRichText(False)
self.setReadOnly(True)
config.instance.changed.connect(self._update_font)
self._update_font()
self.setFocusPolicy(Qt.ClickFocus)
def __repr__(self):
return utils.get_repr(self)
@config.change_filter('fonts.debug_console')
def _update_font(self):
"""Update font when config changed."""
self.setFont(config.val.fonts.debug_console)
def append_text(self, text):
"""Append new text and scroll output to bottom.
@ -157,6 +143,12 @@ class ConsoleWidget(QWidget):
_interpreter: The InteractiveInterpreter to execute code with.
"""
STYLESHEET = """
ConsoleWidget > ConsoleTextEdit, ConsoleWidget > ConsoleLineEdit {
font: {{ conf.fonts.debug_console }};
}
"""
def __init__(self, parent=None):
super().__init__(parent)
if not hasattr(sys, 'ps1'):
@ -182,6 +174,7 @@ class ConsoleWidget(QWidget):
self._vbox.setSpacing(0)
self._vbox.addWidget(self._output)
self._vbox.addWidget(self._lineedit)
stylesheet.set_register(self)
self.setLayout(self._vbox)
self._lineedit.setFocus()
self._interpreter = code.InteractiveInterpreter(namespace)

View File

@ -125,9 +125,13 @@ class _CrashDialog(QDialog):
self.setWindowTitle("Whoops!")
self.resize(QSize(640, 600))
self._vbox = QVBoxLayout(self)
http_client = httpclient.HTTPClient()
self._paste_client = pastebin.PastebinClient(http_client, self)
self._pypi_client = autoupdate.PyPIVersionClient(self)
self._paste_client.success.connect(self.on_paste_success)
self._paste_client.error.connect(self.show_error)
self._init_text()
self._init_contact_input()
@ -246,7 +250,7 @@ class _CrashDialog(QDialog):
except Exception:
self._crash_info.append(("Launch time", traceback.format_exc()))
try:
self._crash_info.append(("Version info", version.version()))
self._crash_info.append(("Version info", version.version_info()))
except Exception:
self._crash_info.append(("Version info", traceback.format_exc()))
try:
@ -296,13 +300,17 @@ class _CrashDialog(QDialog):
except Exception:
log.misc.exception("Failed to save contact information!")
def report(self):
"""Paste the crash info into the pastebin."""
def report(self, *, info=None, contact=None):
"""Paste the crash info into the pastebin.
If info/contact are given as arguments, they override the values
entered in the dialog.
"""
lines = []
lines.append("========== Report ==========")
lines.append(self._info.toPlainText())
lines.append(info or self._info.toPlainText())
lines.append("========== Contact ==========")
lines.append(self._contact.toPlainText())
lines.append(contact or self._contact.toPlainText())
lines.append("========== Debug log ==========")
lines.append(self._debug_log.toPlainText())
self._paste_text = '\n\n'.join(lines)
@ -326,8 +334,6 @@ class _CrashDialog(QDialog):
self._btn_report.setEnabled(False)
self._btn_cancel.setEnabled(False)
self._btn_report.setText("Reporting...")
self._paste_client.success.connect(self.on_paste_success)
self._paste_client.error.connect(self.show_error)
self.report()
@pyqtSlot()
@ -650,7 +656,7 @@ def dump_exception_info(exc, pages, cmdhist, qobjects):
print(''.join(traceback.format_exception(*exc)), file=sys.stderr)
print("\n---- Version info ----", file=sys.stderr)
try:
print(version.version(), file=sys.stderr)
print(version.version_info(), file=sys.stderr)
except Exception:
traceback.print_exc()
print("\n---- Config ----", file=sys.stderr)

View File

@ -162,14 +162,25 @@ class CrashHandler(QObject):
earlyinit.init_faulthandler(self._crash_log_file)
@cmdutils.register(instance='crash-handler')
def report(self):
"""Report a bug in qutebrowser."""
def report(self, info=None, contact=None):
"""Report a bug in qutebrowser.
Args:
info: Information about the bug report. If given, no report dialog
shows up.
contact: Contact information for the report.
"""
pages = self._recover_pages()
cmd_history = objreg.get('command-history')[-5:]
all_objects = debug.get_all_objects()
self._crash_dialog = crashdialog.ReportDialog(pages, cmd_history,
all_objects)
self._crash_dialog.show()
if info is None:
self._crash_dialog.show()
else:
self._crash_dialog.report(info=info, contact=contact)
@pyqtSlot()
def shutdown(self):

View File

@ -36,7 +36,7 @@ from qutebrowser.utils import log, usertypes, error, standarddir, utils
CONNECT_TIMEOUT = 100 # timeout for connecting/disconnecting
WRITE_TIMEOUT = 1000
READ_TIMEOUT = 5000
ATIME_INTERVAL = 60 * 60 * 3 * 1000 # 3 hours
ATIME_INTERVAL = 5000 * 60 # 5 minutes
PROTOCOL_VERSION = 1
@ -397,8 +397,16 @@ class IPCServer(QObject):
if not path:
log.ipc.error("In update_atime with no server path!")
return
log.ipc.debug("Touching {}".format(path))
os.utime(path)
try:
os.utime(path)
except OSError:
log.ipc.exception("Failed to update IPC socket, trying to "
"re-listen...")
self._server.close()
self.listen()
@pyqtSlot()
def shutdown(self):

View File

@ -82,8 +82,8 @@ class KeyHintView(QLabel):
self.update_geometry.emit()
super().showEvent(e)
@pyqtSlot(str)
def update_keyhint(self, modename, prefix):
@pyqtSlot(usertypes.KeyMode, str)
def update_keyhint(self, mode, prefix):
"""Show hints for the given prefix (or hide if prefix is empty).
Args:
@ -108,7 +108,7 @@ class KeyHintView(QLabel):
cmd = objects.commands.get(cmdname)
return cmd and cmd.takes_count()
bindings_dict = config.key_instance.get_bindings_for(modename)
bindings_dict = config.key_instance.get_bindings_for(mode.name)
bindings = [(k, v) for (k, v) in sorted(bindings_dict.items())
if keyutils.KeySequence.parse(prefix).matches(k) and
not blacklisted(str(k)) and

View File

@ -224,24 +224,6 @@ def log_capacity(capacity: int) -> None:
log.ram_handler.change_log_capacity(capacity)
@cmdutils.register(debug=True)
@cmdutils.argument('level', choices=sorted(
(level.lower() for level in log.LOG_LEVELS),
key=lambda e: log.LOG_LEVELS[e.upper()]))
def debug_log_level(level: str) -> None:
"""Change the log level for console logging.
Args:
level: The log level to set.
"""
if log.console_handler is None:
raise cmdutils.CommandError("No log.console_handler. Not attached "
"to a console?")
log.change_console_formatter(log.LOG_LEVELS[level.upper()])
log.console_handler.setLevel(log.LOG_LEVELS[level.upper()])
@cmdutils.register(debug=True)
def debug_log_filter(filters: str) -> None:
"""Change the log filter for console logging.
@ -254,16 +236,12 @@ def debug_log_filter(filters: str) -> None:
raise cmdutils.CommandError("No log.console_filter. Not attached "
"to a console?")
if filters.strip().lower() == 'none':
log.console_filter.names = None
return
try:
new_filter = log.LogFilter.parse(filters)
except log.InvalidLogFilterError as e:
raise cmdutils.CommandError(e)
if not set(filters.split(',')).issubset(log.LOGGER_NAMES):
raise cmdutils.CommandError("filters: Invalid value {} - expected one "
"of: {}".format(
filters, ', '.join(log.LOGGER_NAMES)))
log.console_filter.names = filters.split(',')
log.console_filter.update_from(new_filter)
@cmdutils.register()

View File

@ -96,7 +96,7 @@ def get_argparser():
debug = parser.add_argument_group('debug arguments')
debug.add_argument('-l', '--loglevel', dest='loglevel',
help="Set loglevel", default='info',
help="Override the configured console loglevel",
choices=['critical', 'error', 'warning', 'info',
'debug', 'vdebug'])
debug.add_argument('--logfilter', type=logfilter_error,
@ -150,12 +150,11 @@ def logfilter_error(logfilter):
logfilter: A comma separated list of logger names.
"""
from qutebrowser.utils import log
if set(logfilter.lstrip('!').split(',')).issubset(log.LOGGER_NAMES):
return logfilter
else:
raise argparse.ArgumentTypeError(
"filters: Invalid value {} - expected a list of: {}".format(
logfilter, ', '.join(log.LOGGER_NAMES)))
try:
log.LogFilter.parse(logfilter)
except log.InvalidLogFilterError as e:
raise argparse.ArgumentTypeError(e)
return logfilter
def debug_flag_error(flag):

View File

@ -63,6 +63,7 @@ def handle_fatal_exc(exc: BaseException,
]
log.misc.exception('\n'.join(lines))
else:
log.misc.exception("Fatal exception:")
if pre_text:
msg_text = '{}: {}'.format(pre_text, exc)
else:

View File

@ -41,6 +41,9 @@ try:
except ImportError:
colorama = None
if typing.TYPE_CHECKING:
from qutebrowser.config import config as configmodule
_log_inited = False
_args = None
@ -176,7 +179,7 @@ def stub(suffix: str = '') -> None:
def init_log(args: argparse.Namespace) -> None:
"""Init loggers based on the argparse namespace passed."""
level = args.loglevel.upper()
level = (args.loglevel or "info").upper()
try:
numeric_level = getattr(logging, level)
except AttributeError:
@ -190,16 +193,7 @@ def init_log(args: argparse.Namespace) -> None:
root = logging.getLogger()
global console_filter
if console is not None:
if not args.logfilter:
negate = False
names = None
elif args.logfilter.startswith('!'):
negate = True
names = args.logfilter[1:].split(',')
else:
negate = False
names = args.logfilter.split(',')
console_filter = LogFilter(names, negate)
console_filter = LogFilter.parse(args.logfilter)
console.addFilter(console_filter)
root.addHandler(console)
if ram is not None:
@ -293,7 +287,7 @@ def _init_handlers(
ram_handler = None
else:
ram_handler = RAMHandler(capacity=ram_capacity)
ram_handler.setLevel(logging.NOTSET)
ram_handler.setLevel(logging.DEBUG)
ram_handler.setFormatter(ram_fmt)
ram_handler.html_formatter = html_fmt
@ -526,6 +520,36 @@ def hide_qt_warning(pattern: str, logger: str = 'qt') -> typing.Iterator[None]:
logger_obj.removeFilter(log_filter)
def init_from_config(conf: 'configmodule.ConfigContainer') -> None:
"""Initialize logging settings from the config.
init_log is called before the config module is initialized, so config-based
initialization cannot be performed there.
Args:
conf: The global ConfigContainer.
This is passed rather than accessed via the module to avoid a
cyclic import.
"""
assert _args is not None
if _args.debug:
init.debug("--debug flag overrides log configs")
return
if ram_handler:
ramlevel = conf.logging.level.ram
init.debug("Configuring RAM loglevel to %s", ramlevel)
ram_handler.setLevel(LOG_LEVELS[ramlevel.upper()])
if console_handler:
consolelevel = conf.logging.level.console
if _args.loglevel:
init.debug("--loglevel flag overrides logging.level.console")
else:
init.debug("Configuring console loglevel to %s", consolelevel)
level = LOG_LEVELS[consolelevel.upper()]
console_handler.setLevel(level)
change_console_formatter(level)
class QtWarningFilter(logging.Filter):
"""Filter to filter Qt warnings.
@ -544,6 +568,17 @@ class QtWarningFilter(logging.Filter):
return do_log
class InvalidLogFilterError(Exception):
"""Raised when an invalid filter string is passed to LogFilter.parse()."""
def __init__(self, names: typing.Set[str]):
invalid = names - set(LOGGER_NAMES)
super().__init__("Invalid log category {} - valid categories: {}"
.format(', '.join(sorted(invalid)),
', '.join(LOGGER_NAMES)))
class LogFilter(logging.Filter):
"""Filter to filter log records based on the commandline argument.
@ -552,30 +587,58 @@ class LogFilter(logging.Filter):
comma-separated list instead.
Attributes:
names: A list of record names to filter.
negated: Whether names is a list of records to log or to suppress.
names: A set of logging names to allow.
negated: Whether names is a set of names to log or to suppress.
only_debug: Only filter debug logs, always show anything more important
than debug.
"""
def __init__(self, names: typing.Optional[typing.Iterable[str]],
negate: bool = False) -> None:
def __init__(self, names: typing.Set[str], *, negated: bool = False,
only_debug: bool = True) -> None:
super().__init__()
self.names = names
self.negated = negate
self.negated = negated
self.only_debug = only_debug
@classmethod
def parse(cls, filter_str: typing.Optional[str], *,
only_debug: bool = True) -> 'LogFilter':
"""Parse a log filter from a string."""
if filter_str is None or filter_str == 'none':
names = set()
negated = False
else:
filter_str = filter_str.lower()
if filter_str.startswith('!'):
negated = True
filter_str = filter_str[1:]
else:
negated = False
names = {e.strip() for e in filter_str.split(',')}
if not names.issubset(LOGGER_NAMES):
raise InvalidLogFilterError(names)
return cls(names=names, negated=negated, only_debug=only_debug)
def update_from(self, other: 'LogFilter') -> None:
"""Update this filter's properties from another filter."""
self.names = other.names
self.negated = other.negated
self.only_debug = other.only_debug
def filter(self, record: logging.LogRecord) -> bool:
"""Determine if the specified record is to be logged."""
if self.names is None:
if not self.names:
# No filter
return True
if record.levelno > logging.DEBUG:
elif record.levelno > logging.DEBUG and self.only_debug:
# More important than DEBUG, so we won't filter at all
return True
for name in self.names:
if record.name == name:
return not self.negated
elif not record.name.startswith(name):
continue
elif record.name[len(name)] == '.':
return not self.negated
elif record.name.split('.')[0] in self.names:
return not self.negated
return self.negated
@ -601,19 +664,26 @@ class RAMHandler(logging.Handler):
self._data = collections.deque()
def emit(self, record: logging.LogRecord) -> None:
if record.levelno >= logging.DEBUG:
# We don't log VDEBUG to RAM.
self._data.append(record)
self._data.append(record)
def dump_log(self, html: bool = False, level: str = 'vdebug') -> str:
def dump_log(self, html: bool = False, level: str = 'vdebug',
logfilter: LogFilter = None) -> str:
"""Dump the complete formatted log data as string.
FIXME: We should do all the HTML formatter via jinja2.
FIXME: We should do all the HTML formatting via jinja2.
(probably obsolete when moving to a widget for logging,
https://github.com/qutebrowser/qutebrowser/issues/34
Args:
html: Produce HTML rather than plaintext output.
level: The minimal loglevel to show.
logfilter: A LogFilter instance used to filter log lines.
"""
minlevel = LOG_LEVELS.get(level.upper(), VDEBUG_LEVEL)
if logfilter is None:
logfilter = LogFilter(set())
if html:
assert self.html_formatter is not None
fmt = self.html_formatter.format
@ -624,7 +694,8 @@ class RAMHandler(logging.Handler):
try:
lines = [fmt(record)
for record in self._data
if record.levelno >= minlevel]
if record.levelno >= minlevel and
logfilter.filter(record)]
finally:
self.release()
return '\n'.join(lines)

View File

@ -25,7 +25,7 @@
import traceback
import typing
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from PyQt5.QtCore import pyqtSignal, QObject
from qutebrowser.utils import usertypes, log, utils

View File

@ -35,7 +35,7 @@ import typing
import pkg_resources
from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray,
QIODevice, QSaveFile, QT_VERSION_STR,
QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR,
PYQT_VERSION_STR, QObject, QUrl)
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QApplication
@ -44,9 +44,6 @@ try:
except ImportError: # pragma: no cover
qWebKitVersion = None # type: ignore[assignment] # noqa: N816
if typing.TYPE_CHECKING:
from PyQt5.QtCore import QFileDevice
from qutebrowser.misc import objects
from qutebrowser.utils import usertypes
@ -74,13 +71,17 @@ class QtOSError(OSError):
if msg is None:
msg = dev.errorString()
self.qt_errno = None # type: typing.Optional[QFileDevice.FileError]
if isinstance(dev, QFileDevice):
msg = self._init_filedev(dev, msg)
super().__init__(msg)
self.qt_errno = None # type: typing.Optional[QFileDevice.FileError]
try:
self.qt_errno = dev.error()
except AttributeError:
pass
def _init_filedev(self, dev: QFileDevice, msg: str) -> str:
self.qt_errno = dev.error()
filename = dev.fileName()
msg += ": {!r}".format(filename)
return msg
def version_check(version: str,
@ -229,7 +230,7 @@ def savefile_open(
if not open_ok:
raise QtOSError(f)
dev = typing.cast(typing.IO[bytes], PyQIODevice(f))
dev = typing.cast(typing.BinaryIO, PyQIODevice(f))
if binary:
new_f = dev # type: typing.IO

View File

@ -256,6 +256,10 @@ class KeyMode(enum.Enum):
jump_mark = 10
record_macro = 11
run_macro = 12
# 'register' is a bit of an oddball here: It's not really a "real" mode,
# but it's used in the config for common bindings for
# set_mark/jump_mark/record_macro/run_macro.
register = 13
class Exit(enum.IntEnum):

View File

@ -36,6 +36,8 @@ import shlex
import glob
import mimetypes
import typing
import ctypes
import ctypes.util
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QColor, QClipboard, QDesktopServices
@ -776,3 +778,16 @@ def ceil_log(number: int, base: int) -> int:
result += 1
accum *= base
return result
def libgl_workaround() -> None:
"""Work around QOpenGLShaderProgram issues, especially for Nvidia.
See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
"""
if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'):
return
libgl = ctypes.util.find_library("GL")
if libgl is not None: # pragma: no branch
ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL)

View File

@ -31,6 +31,7 @@ import enum
import datetime
import getpass
import typing
import functools
import attr
import pkg_resources
@ -160,6 +161,14 @@ def _git_str() -> typing.Optional[str]:
return None
def _call_git(gitpath: str, *args: str) -> str:
"""Call a git subprocess."""
return subprocess.run(
['git'] + list(args),
cwd=gitpath, check=True,
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
def _git_str_subprocess(gitpath: str) -> typing.Optional[str]:
"""Try to get the git commit ID and timestamp by calling git.
@ -173,15 +182,11 @@ def _git_str_subprocess(gitpath: str) -> typing.Optional[str]:
return None
try:
# https://stackoverflow.com/questions/21017300/21017394#21017394
commit_hash = subprocess.run(
['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'],
cwd=gitpath, check=True,
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
date = subprocess.run(
['git', 'show', '-s', '--format=%ci', 'HEAD'],
cwd=gitpath, check=True,
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
return '{} ({})'.format(commit_hash, date)
commit_hash = _call_git(gitpath, 'describe', '--match=NeVeRmAtCh',
'--always', '--dirty')
date = _call_git(gitpath, 'show', '-s', '--format=%ci', 'HEAD')
branch = _call_git(gitpath, 'rev-parse', '--abbrev-ref', 'HEAD')
return '{} on {} ({})'.format(commit_hash, branch, date)
except (subprocess.CalledProcessError, OSError):
return None
@ -233,7 +238,7 @@ def _module_versions() -> typing.Sequence[str]:
for modname, attributes in modules.items():
try:
module = importlib.import_module(modname)
except ImportError:
except (ImportError, ValueError):
text = '{}: no'.format(modname)
else:
for name in attributes:
@ -411,7 +416,7 @@ def _config_py_loaded() -> str:
return "no config.py was loaded"
def version() -> str:
def version_info() -> str:
"""Return a string with various version information."""
lines = ["qutebrowser v{}".format(qutebrowser.__version__)]
gitver = _git_str()
@ -442,8 +447,8 @@ def version() -> str:
if qapp:
style = qapp.style()
lines.append('Style: {}'.format(style.metaObject().className()))
platform_name = qapp.platformName()
lines.append('Platform plugin: {}'.format(platform_name))
lines.append('Platform plugin: {}'.format(qapp.platformName()))
lines.append('OpenGL: {}'.format(opengl_info()))
importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__))
@ -487,7 +492,65 @@ def version() -> str:
return '\n'.join(lines)
def opengl_vendor() -> typing.Optional[str]: # pragma: no cover
@attr.s
class OpenGLInfo:
"""Information about the OpenGL setup in use."""
# If we're using OpenGL ES. If so, no further information is available.
gles = attr.ib(False) # type: bool
# The name of the vendor. Examples:
# - nouveau
# - "Intel Open Source Technology Center", "Intel", "Intel Inc."
vendor = attr.ib(None) # type: typing.Optional[str]
# The OpenGL version as a string. See tests for examples.
version_str = attr.ib(None) # type: typing.Optional[str]
# The parsed version as a (major, minor) tuple of ints
version = attr.ib(None) # type: typing.Optional[typing.Tuple[int, ...]]
# The vendor specific information following the version number
vendor_specific = attr.ib(None) # type: typing.Optional[str]
def __str__(self) -> str:
if self.gles:
return 'OpenGL ES'
return '{}, {}'.format(self.vendor, self.version_str)
@classmethod
def parse(cls, *, vendor: str, version: str) -> 'OpenGLInfo':
"""Parse OpenGL version info from a string.
The arguments should be the strings returned by OpenGL for GL_VENDOR
and GL_VERSION, respectively.
According to the OpenGL reference, the version string should have the
following format:
<major>.<minor>[.<release>] <vendor-specific info>
"""
if ' ' not in version:
log.misc.warning("Failed to parse OpenGL version (missing space): "
"{}".format(version))
return cls(vendor=vendor, version_str=version)
num_str, vendor_specific = version.split(' ', maxsplit=1)
try:
parsed_version = tuple(int(i) for i in num_str.split('.'))
except ValueError:
log.misc.warning("Failed to parse OpenGL version (parsing int): "
"{}".format(version))
return cls(vendor=vendor, version_str=version)
return cls(vendor=vendor, version_str=version,
version=parsed_version, vendor_specific=vendor_specific)
@functools.lru_cache(maxsize=1)
def opengl_info() -> typing.Optional[OpenGLInfo]: # pragma: no cover
"""Get the OpenGL vendor used.
This returns a string such as 'nouveau' or
@ -496,10 +559,14 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover
"""
assert QApplication.instance()
override = os.environ.get('QUTE_FAKE_OPENGL_VENDOR')
# Some setups can segfault in here if we don't do this.
utils.libgl_workaround()
override = os.environ.get('QUTE_FAKE_OPENGL')
if override is not None:
log.init.debug("Using override {}".format(override))
return override
vendor, version = override.split(', ', maxsplit=1)
return OpenGLInfo.parse(vendor=vendor, version=version)
old_context = typing.cast(typing.Optional[QOpenGLContext],
QOpenGLContext.currentContext())
@ -522,7 +589,7 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover
try:
if ctx.isOpenGLES():
# Can't use versionFunctions there
return None
return OpenGLInfo(gles=True)
vp = QOpenGLVersionProfile()
vp.setVersion(2, 0)
@ -537,7 +604,10 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover
log.init.debug("Getting version functions failed!")
return None
return vf.glGetString(vf.GL_VENDOR)
vendor = vf.glGetString(vf.GL_VENDOR)
version = vf.glGetString(vf.GL_VERSION)
return OpenGLInfo.parse(vendor=vendor, version=version)
finally:
ctx.doneCurrent()
if old_context and old_surface:
@ -580,5 +650,5 @@ def pastebin_version(pbclient: pastebin.PastebinClient = None) -> None:
pbclient.paste(getpass.getuser(),
"qute version info {}".format(qutebrowser.__version__),
version(),
version_info(),
private=True)

View File

@ -20,21 +20,24 @@
"""Generate the html documentation based on the asciidoc files."""
from typing import List, Optional
import re
import os
import os.path
import sys
import subprocess
import glob
import shutil
import tempfile
import argparse
import io
import pathlib
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils
DOC_DIR = pathlib.Path("qutebrowser/html/doc")
class AsciiDoc:
@ -42,31 +45,32 @@ class AsciiDoc:
FILES = ['faq', 'changelog', 'contributing', 'quickstart', 'userscripts']
def __init__(self, asciidoc, website):
self._cmd = None
def __init__(self,
asciidoc: Optional[List[str]],
website: Optional[str]) -> None:
self._cmd = None # type: Optional[List[str]]
self._asciidoc = asciidoc
self._website = website
self._homedir = None
self._themedir = None
self._tempdir = None
self._homedir = None # type: Optional[pathlib.Path]
self._themedir = None # type: Optional[pathlib.Path]
self._tempdir = None # type: Optional[pathlib.Path]
self._failed = False
def prepare(self):
def prepare(self) -> None:
"""Get the asciidoc command and create the homedir to use."""
self._cmd = self._get_asciidoc_cmd()
self._homedir = tempfile.mkdtemp()
self._themedir = os.path.join(
self._homedir, '.asciidoc', 'themes', 'qute')
self._tempdir = os.path.join(self._homedir, 'tmp')
os.makedirs(self._tempdir)
os.makedirs(self._themedir)
self._homedir = pathlib.Path(tempfile.mkdtemp())
self._themedir = self._homedir / '.asciidoc' / 'themes' / 'qute'
self._tempdir = self._homedir / 'tmp'
self._tempdir.mkdir(parents=True)
self._themedir.mkdir(parents=True)
def cleanup(self):
def cleanup(self) -> None:
"""Clean up the temporary home directory for asciidoc."""
if self._homedir is not None and not self._failed:
shutil.rmtree(self._homedir)
def build(self):
def build(self) -> None:
"""Build either the website or the docs."""
if self._website:
self._build_website()
@ -74,14 +78,12 @@ class AsciiDoc:
self._build_docs()
self._copy_images()
def _build_docs(self):
def _build_docs(self) -> None:
"""Render .asciidoc files to .html sites."""
files = [('doc/{}.asciidoc'.format(f),
'qutebrowser/html/doc/{}.html'.format(f))
for f in self.FILES]
for src in glob.glob('doc/help/*.asciidoc'):
name, _ext = os.path.splitext(os.path.basename(src))
dst = 'qutebrowser/html/doc/{}.html'.format(name)
files = [(pathlib.Path('doc/{}.asciidoc'.format(f)),
DOC_DIR / (f + ".html")) for f in self.FILES]
for src in pathlib.Path('doc/help/').glob('*.asciidoc'):
dst = DOC_DIR / (src.stem + ".html")
files.append((src, dst))
# patch image links to use local copy
@ -94,8 +96,8 @@ class AsciiDoc:
asciidoc_args = ['-a', 'source-highlighter=pygments']
for src, dst in files:
src_basename = os.path.basename(src)
modified_src = os.path.join(self._tempdir, src_basename)
assert self._tempdir is not None # for mypy
modified_src = self._tempdir / src.name
with open(modified_src, 'w', encoding='utf-8') as modified_f, \
open(src, 'r', encoding='utf-8') as f:
for line in f:
@ -104,34 +106,26 @@ class AsciiDoc:
modified_f.write(line)
self.call(modified_src, dst, *asciidoc_args)
def _copy_images(self):
def _copy_images(self) -> None:
"""Copy image files to qutebrowser/html/doc."""
print("Copying files...")
dst_path = os.path.join('qutebrowser', 'html', 'doc', 'img')
try:
os.mkdir(dst_path)
except FileExistsError:
pass
dst_path = DOC_DIR / 'img'
dst_path.mkdir(exist_ok=True)
for filename in ['cheatsheet-big.png', 'cheatsheet-small.png']:
src = os.path.join('doc', 'img', filename)
dst = os.path.join(dst_path, filename)
src = pathlib.Path('doc') / 'img' / filename
dst = dst_path / filename
shutil.copy(src, dst)
def _build_website_file(self, root, filename):
def _build_website_file(self, root: pathlib.Path, filename: str) -> None:
"""Build a single website file."""
src = os.path.join(root, filename)
src_basename = os.path.basename(src)
parts = [self._website[0]]
dirname = os.path.dirname(src)
if dirname:
parts.append(os.path.relpath(os.path.dirname(src)))
parts.append(
os.extsep.join((os.path.splitext(src_basename)[0],
'html')))
dst = os.path.join(*parts)
os.makedirs(os.path.dirname(dst), exist_ok=True)
src = root / filename
assert self._website is not None # for mypy
dst = pathlib.Path(self._website)
dst = dst / src.parent.relative_to('.') / (src.stem + ".html")
dst.parent.mkdir(exist_ok=True)
modified_src = os.path.join(self._tempdir, src_basename)
assert self._tempdir is not None # for mypy
modified_src = self._tempdir / src.name
shutil.copy('www/header.asciidoc', modified_src)
outfp = io.StringIO()
@ -187,25 +181,24 @@ class AsciiDoc:
'-a', 'source-highlighter=pygments']
self.call(modified_src, dst, *asciidoc_args)
def _build_website(self):
def _build_website(self) -> None:
"""Prepare and build the website."""
theme_file = os.path.abspath(os.path.join('www', 'qute.css'))
theme_file = (pathlib.Path('www') / 'qute.css').resolve()
assert self._themedir is not None # for mypy
shutil.copy(theme_file, self._themedir)
outdir = self._website[0]
assert self._website is not None # for mypy
outdir = pathlib.Path(self._website)
for root, _dirs, files in os.walk(os.getcwd()):
for filename in files:
basename, ext = os.path.splitext(filename)
if (ext != '.asciidoc' or
basename in ['header', 'OpenSans-License']):
continue
self._build_website_file(root, filename)
for item_path in pathlib.Path().rglob('*.asciidoc'):
if item_path.stem in ['header', 'OpenSans-License']:
continue
self._build_website_file(item_path.parent, item_path.name)
copy = {'icons': 'icons', 'doc/img': 'doc/img', 'www/media': 'media/'}
for src, dest in copy.items():
full_dest = os.path.join(outdir, dest)
full_dest = outdir / dest
try:
shutil.rmtree(full_dest)
except FileNotFoundError:
@ -214,13 +207,15 @@ class AsciiDoc:
for dst, link_name in [
('README.html', 'index.html'),
(os.path.join('doc', 'quickstart.html'), 'quickstart.html')]:
((pathlib.Path('doc') / 'quickstart.html'),
'quickstart.html')]:
assert isinstance(dst, (str, pathlib.Path)) # for mypy
try:
os.symlink(dst, os.path.join(outdir, link_name))
(outdir / link_name).symlink_to(dst)
except FileExistsError:
pass
def _get_asciidoc_cmd(self):
def _get_asciidoc_cmd(self) -> List[str]:
"""Try to find out what commandline to use to invoke asciidoc."""
if self._asciidoc is not None:
return self._asciidoc
@ -243,7 +238,7 @@ class AsciiDoc:
raise FileNotFoundError
def call(self, src, dst, *args):
def call(self, src: pathlib.Path, dst: pathlib.Path, *args):
"""Call asciidoc for the given files.
Args:
@ -251,15 +246,16 @@ class AsciiDoc:
dst: The destination .html file, or None to auto-guess.
*args: Additional arguments passed to asciidoc.
"""
print("Calling asciidoc for {}...".format(os.path.basename(src)))
print("Calling asciidoc for {}...".format(src.name))
assert self._cmd is not None # for mypy
cmdline = self._cmd[:]
if dst is not None:
cmdline += ['--out-file', dst]
cmdline += ['--out-file', str(dst)]
cmdline += args
cmdline.append(src)
cmdline.append(str(src))
try:
env = os.environ.copy()
env['HOME'] = self._homedir
env['HOME'] = str(self._homedir)
subprocess.run(cmdline, check=True, env=env)
except (subprocess.CalledProcessError, OSError) as e:
self._failed = True
@ -269,11 +265,11 @@ class AsciiDoc:
sys.exit(1)
def parse_args():
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser()
parser.add_argument('--website', help="Build website into a given "
"directory.", nargs=1)
"directory.")
parser.add_argument('--asciidoc', help="Full path to python and "
"asciidoc.py. If not given, it's searched in PATH.",
nargs=2, required=False,
@ -281,12 +277,9 @@ def parse_args():
return parser.parse_args()
def run(**kwargs):
def run(**kwargs) -> None:
"""Regenerate documentation."""
try:
os.mkdir('qutebrowser/html/doc')
except FileExistsError:
pass
DOC_DIR.mkdir(exist_ok=True)
asciidoc = AsciiDoc(**kwargs)
try:
@ -303,7 +296,7 @@ def run(**kwargs):
asciidoc.cleanup()
def main(colors=False):
def main(colors: bool = False) -> None:
"""Generate html files for the online documentation."""
utils.change_cwd()
utils.use_color = colors

View File

@ -91,7 +91,7 @@ def check_spelling():
'[Mm]ininum', '[Rr]esett?ed', '[Rr]ecieved', '[Rr]egularily',
'[Uu]nderlaying', '[Ii]nexistant', '[Ee]lipsis', 'commiting',
'existant', '[Rr]esetted', '[Ss]imilarily', '[Ii]nformations',
'[Aa]n [Uu][Rr][Ll]'}
'[Aa]n [Uu][Rr][Ll]', '[Tt]reshold'}
# Words which look better when splitted, but might need some fine tuning.
words |= {'[Ww]ebelements', '[Mm]ouseevent', '[Kk]eysequence',

View File

@ -59,6 +59,11 @@ class UsageFormatter(argparse.HelpFormatter):
argparse.HelpFormatter while copying 99% of the code :-/
"""
def __init__(self, prog, indent_increment=2, max_help_position=24,
width=200):
"""Override __init__ to set a fixed width as default."""
super().__init__(prog, indent_increment, max_help_position, width)
def _format_usage(self, usage, actions, groups, _prefix):
"""Override _format_usage to not add the 'usage:' prefix."""
return super()._format_usage(usage, actions, groups, '')

View File

@ -38,6 +38,14 @@ BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
os.path.pardir)
def _call_git(gitpath, *args):
"""Call a git subprocess."""
return subprocess.run(
['git'] + list(args),
cwd=gitpath, check=True,
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
def _git_str():
"""Try to find out git version.
@ -51,15 +59,11 @@ def _git_str():
return None
try:
# https://stackoverflow.com/questions/21017300/21017394#21017394
commit_hash = subprocess.run(
['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'],
cwd=BASEDIR, check=True,
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
date = subprocess.run(
['git', 'show', '-s', '--format=%ci', 'HEAD'],
cwd=BASEDIR, check=True,
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
return '{} ({})'.format(commit_hash, date)
commit_hash = _call_git(BASEDIR, 'describe', '--match=NeVeRmAtCh',
'--always', '--dirty')
date = _call_git(BASEDIR, 'show', '-s', '--format=%ci', 'HEAD')
branch = _call_git(BASEDIR, 'rev-parse', '--abbrev-ref', 'HEAD')
return '{} on {} ({})'.format(commit_hash, branch, date)
except (subprocess.CalledProcessError, OSError):
return None

View File

@ -25,8 +25,6 @@ import os
import sys
import warnings
import pathlib
import ctypes
import ctypes.util
import pytest
import hypothesis
@ -258,9 +256,7 @@ def set_backend(monkeypatch, request):
@pytest.fixture(autouse=True, scope='session')
def apply_libgl_workaround():
"""Make sure we load libGL early so QtWebEngine tests run properly."""
libgl = ctypes.util.find_library("GL")
if libgl is not None:
ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL)
utils.libgl_workaround()
@pytest.fixture(autouse=True)

View File

@ -5,7 +5,8 @@ Feature: Caret mode
Background:
Given I open data/caret.html
And I run :tab-only ;; enter-mode caret
And I run :tab-only
And I also run :enter-mode caret
# :yank selection

View File

@ -152,6 +152,17 @@ def run_command_given(quteproc, command):
quteproc.send_cmd(command)
@bdd.given(bdd.parsers.parse("I also run {command}"))
def run_command_given_2(quteproc, command):
"""Run a qutebrowser command.
Separate from the above as a hack to run two commands in a Background
without having to use ";;". This is needed because pytest-bdd doesn't allow
re-using a Given step...
"""
quteproc.send_cmd(command)
@bdd.given("I have a fresh instance")
def fresh_instance(quteproc):
"""Restart qutebrowser instance for tests needing a fresh state."""

View File

@ -323,8 +323,8 @@ Feature: Using hints
And I wait until data/hello.txt is loaded
And I press the key ","
# Waiting here so we don't affect the next test
And I wait for "Releasing inhibition state of normal mode." in the log
Then "Ignoring key ',', because the normal mode is currently inhibited." should be logged
And I wait for "NormalKeyParser for mode normal: Releasing inhibition state of normal mode." in the log
Then "NormalKeyParser for mode normal: Ignoring key ',', because the normal mode is currently inhibited." should be logged
Scenario: Turning off auto_follow_timeout
When I set hints.auto_follow_timeout to 0

View File

@ -153,7 +153,7 @@ Feature: Using private browsing
- url: http://localhost:*/data/numbers/1.txt
- url: http://localhost:*/data/numbers/2.txt
@flaky
Scenario: Saving a private session with only-active-window
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab

View File

@ -258,20 +258,37 @@ Feature: Special qute:// pages
And the page should contain the plaintext "the-warning-message"
And the page should contain the plaintext "the-info-message"
Scenario: Showing messages of category 'message'
When I run :message-info the-info-message
And I run :messages -f message
Then qute://log/?level=info&logfilter=message should be loaded
And the page should contain the plaintext "the-info-message"
Scenario: Showing messages of category 'misc'
When I run :message-info the-info-message
And I run :messages -f misc
Then qute://log/?level=info&logfilter=misc should be loaded
And the page should not contain the plaintext "the-info-message"
@qtwebengine_flaky
Scenario: Showing messages of an invalid level
When I run :messages cataclysmic
Then the error "Invalid log level cataclysmic!" should be shown
Scenario: Showing messages with an invalid category
When I run :messages -f invalid
Then the error "Invalid log category invalid - *" should be shown
Scenario: Using qute://log directly
When I open qute://log without waiting
# With Qt 5.9, we don't get a loaded message?
And I wait for "Changing title for idx * to 'log'" in the log
Then no crash should happen
Scenario: Using qute://plainlog directly
When I open qute://plainlog
Then no crash should happen
# FIXME More possible tests:
# :message --plain
# Using qute://log directly with invalid category
# same with invalid level
# :version

View File

@ -282,6 +282,7 @@ Feature: Saving and loading sessions
Then "Saved session quiet_session." should be logged with level debug
And the session quiet_session should exist
@flaky
Scenario: Saving session with --only-active-window
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab

View File

@ -166,13 +166,9 @@ Feature: Miscellaneous utility commands exposed to the user.
# Other :debug-log-{level,filter} features are tested in
# unit/utils/test_log.py as using them would break end2end tests.
Scenario: Using debug-log-level with invalid level
When I run :debug-log-level hello
Then the error "level: Invalid value hello - expected one of: vdebug, debug, info, warning, error, critical" should be shown
Scenario: Using debug-log-filter with invalid filter
When I run :debug-log-filter blah
Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown
Then the error "Invalid log category blah - valid categories: statusbar, *" should be shown
Scenario: Using debug-log-filter
When I run :debug-log-filter commands,ipc,webview

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