Merge branch 'master' into docked-inspector
This commit is contained in:
commit
1c01420aec
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 1.11.1
|
||||
current_version = 1.12.0
|
||||
commit = True
|
||||
message = Release v{new_version}
|
||||
tag = True
|
||||
|
|
|
|||
|
|
@ -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`?**:
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
20
.travis.yml
20
.travis.yml
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
--------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'+
|
||||
|
|
|
|||
|
|
@ -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:[<Ctrl-6>]+: +pass:[<Ctrl-^>]+
|
||||
- +pass:[<Ctrl-Enter>]+: +pass:[<Ctrl-Return>]+
|
||||
- +pass:[<Ctrl-I>]+: +pass:[<Tab>]+
|
||||
- +pass:[<Ctrl-J>]+: +pass:[<Return>]+
|
||||
- +pass:[<Ctrl-M>]+: +pass:[<Return>]+
|
||||
- +pass:[<Ctrl-[>]+: +pass:[<Escape>]+
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
^^^^^^^^^
|
||||
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,5 +4,4 @@ pympler
|
|||
github3.py
|
||||
bump2version
|
||||
requests
|
||||
lxml
|
||||
pyqt-builder
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#@ filter: PyQt5 < 6
|
||||
#@ filter: PyQtWebEngine < 6
|
||||
PyQt5 >= 5.15, < 6
|
||||
PyQtWebEngine >= 5.15, < 6
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
vulture==1.4
|
||||
vulture==1.5
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
|
|
|||
|
|
@ -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'):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -62,3 +62,4 @@ rules:
|
|||
max-params: "off"
|
||||
prefer-named-capture-group: "off"
|
||||
function-call-argument-newline: "off"
|
||||
no-negated-condition: "off"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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, '')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue