Merge branch 'master' into webengine_caret

This commit is contained in:
Artur Shaik 2017-12-28 20:43:31 +06:00 committed by GitHub
commit 5605d3cd8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
244 changed files with 3725 additions and 1327 deletions

View File

@ -12,6 +12,7 @@ exclude_lines =
def __repr__
raise AssertionError
raise NotImplementedError
raise utils\.Unreachable
if __name__ == ["']__main__["']:
[xml]

30
.flake8
View File

@ -1,5 +1,8 @@
[flake8]
exclude = .*,__pycache__,resources.py
# B001: bare except
# B008: Do not perform calls in argument defaults. (fine with some Qt stuff)
# B305: .next() (false-positives)
# E128: continuation line under-indented for visual indent
# E226: missing whitespace around arithmetic operator
# E265: Block comment should start with '#'
@ -19,32 +22,33 @@ exclude = .*,__pycache__,resources.py
# D103: Missing docstring in public function (will be handled by others)
# D104: Missing docstring in public package (will be handled by others)
# D105: Missing docstring in magic method (will be handled by others)
# D106: Missing docstring in public nested class (will be handled by others)
# D107: Missing docstring in __init__ (will be handled by others)
# D209: Blank line before closing """ (removed from PEP257)
# D211: No blank lines allowed before class docstring
# (PEP257 got changed, but let's stick to the old standard)
# D401: First line should be in imperative mood (okay sometimes)
# D402: First line should not be function's signature (false-positives)
# D403: First word of the first line should be properly capitalized
# (false-positives)
# D413: Missing blank line after last section (not in pep257?)
# A003: Builtin name for class attribute (needed for attrs)
ignore =
B001,B008,B305,
E128,E226,E265,E501,E402,E266,E722,E731,
F401,
N802,
P101,P102,P103,
D102,D103,D104,D105,D209,D211,D402,D403
D102,D103,D106,D107,D104,D105,D209,D211,D401,D402,D403,D413,
A003
min-version = 3.4.0
max-complexity = 12
putty-auto-ignore = True
putty-ignore =
/# pylint: disable=invalid-name/ : +N801,N806
/# pragma: no mccabe/ : +C901
tests/*/test_*.py : +D100,D101,D401
tests/conftest.py : +F403
tests/unit/browser/test_history.py : +N806
tests/helpers/fixtures.py : +N806
tests/unit/browser/webkit/http/test_content_disposition.py : +D400
scripts/dev/ci/appveyor_install.py : +FI53
# FIXME:conf
tests/unit/completion/test_models.py : +F821
per-file-ignores =
tests/*/test_*.py : D100,D101,D401
tests/unit/browser/test_history.py : N806
tests/helpers/fixtures.py : N806
tests/unit/browser/webkit/http/test_content_disposition.py : D400
scripts/dev/ci/appveyor_install.py : FI53
copyright-check = True
copyright-regexp = # Copyright [\d-]+ .*
copyright-min-file-size = 110

46
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mail@qutebrowser.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -4,6 +4,9 @@
- Either run the testsuite locally, or keep an eye on Travis CI / AppVeyor
after pushing changes.
See the full contribution docs for details:
- If you are stuck somewhere or have questions,
https://github.com/qutebrowser/qutebrowser#getting-help[please ask]!
See the full contribution documentation for details and other useful hints:
include::../doc/contributing.asciidoc[]

View File

@ -13,38 +13,33 @@ persistent=n
[MESSAGES CONTROL]
enable=all
disable=no-self-use,
fixme,
global-statement,
locally-disabled,
disable=locally-disabled,
locally-enabled,
too-many-ancestors,
too-few-public-methods,
too-many-public-methods,
suppressed-message,
fixme,
no-self-use,
cyclic-import,
bad-continuation,
too-many-instance-attributes,
blacklisted-name,
too-many-lines,
logging-format-interpolation,
logging-not-lazy,
broad-except,
bare-except,
eval-used,
exec-used,
ungrouped-imports,
suppressed-message,
too-many-return-statements,
duplicate-code,
global-statement,
wrong-import-position,
duplicate-code,
no-else-return,
# https://github.com/PyCQA/pylint/issues/1698
unsupported-membership-test,
unsupported-assignment-operation,
unsubscriptable-object,
too-many-ancestors,
too-many-public-methods,
too-many-instance-attributes,
too-many-lines,
too-many-return-statements,
too-many-boolean-expressions,
too-many-locals,
too-many-branches,
too-many-statements
too-many-statements,
too-few-public-methods
[BASIC]
function-rgx=[a-z_][a-z0-9_]{2,50}$
@ -73,10 +68,10 @@ valid-metaclass-classmethod-first-arg=cls
[TYPECHECK]
ignored-modules=PyQt5,PyQt5.QtWebKit
ignored-classes=_CountingAttr
[IMPORTS]
# WORKAROUND
# For some reason, pylint doesn't know about some Python 3 modules on
# AppVeyor...
known-standard-library=faulthandler,http,enum,tokenize,posixpath,importlib,types
known-third-party=sip

View File

@ -23,7 +23,7 @@ matrix:
env: TESTENV=py36-pyqt59-cov
- os: osx
env: TESTENV=py36 OSX=sierra
osx_image: xcode8.3
osx_image: xcode9.2
language: generic
# https://github.com/qutebrowser/qutebrowser/issues/2013
# - os: osx
@ -52,6 +52,10 @@ matrix:
language: node_js
python: null
node_js: "lts/*"
- os: linux
language: generic
env: TESTENV=shellcheck
services: docker
fast_finish: true
cache:

View File

@ -14,6 +14,7 @@ include qutebrowser/git-commit-id
include LICENSE doc/* README.asciidoc
include misc/qutebrowser.desktop
include misc/qutebrowser.appdata.xml
include misc/Makefile
include requirements.txt
include tox.ini
include qutebrowser.py

View File

@ -15,7 +15,7 @@ image:https://travis-ci.org/qutebrowser/qutebrowser.svg?branch=master["Build Sta
image:https://ci.appveyor.com/api/projects/status/5pyauww2k68bbow2/branch/master?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/qutebrowser/qutebrowser"]
image:https://codecov.io/github/qutebrowser/qutebrowser/coverage.svg?branch=master["coverage badge",link="https://codecov.io/github/qutebrowser/qutebrowser?branch=master"]
link:https://www.qutebrowser.org[website] | link:https://blog.qutebrowser.org[blog] | link:https://github.com/qutebrowser/qutebrowser/releases[releases]
link:https://www.qutebrowser.org[website] | link:https://blog.qutebrowser.org[blog] | https://github.com/qutebrowser/qutebrowser/blob/master/doc/faq.asciidoc[FAQ] | https://www.qutebrowser.org/doc/contributing.html[contributing] | link:https://github.com/qutebrowser/qutebrowser/releases[releases] | https://github.com/qutebrowser/qutebrowser/blob/master/doc/install.asciidoc[installing]
// QUTE_WEB_HIDE_END
qutebrowser is a keyboard-focused browser with a minimal GUI. It's based
@ -109,7 +109,7 @@ The following software and libraries are required to run qutebrowser:
link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is
supported
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
(5.9 recommended) for Python 3
(5.9.2 recommended) for Python 3
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2]

View File

@ -21,6 +21,11 @@ v1.1.0 (unreleased)
Added
~~~~~
- Initial support for Greasemonkey scripts. There are still some rough edges,
but many scripts should already work.
- There's now a `misc/Makefile` file in releases, which should help
distributions which package qutebrowser, as they can run something like
`make -f misc/Makefile DESTDIR="$pkgdir" install` now.
- New `{current_url}` field for `window.title_format` and `tabs.title.format`.
- New `colors.statusbar.passthrough.fg`/`.bg` settings.
- New `completion.delay` and `completion.min_chars` settings to update the
@ -43,16 +48,31 @@ Added
completion item text.
- New `tabs.pinned.shrink` setting to (`true` by default) to make it possible
for pinned tabs and normal tabs to have the same size.
_ New `content.windowed_fullscreen` setting to show e.g. a fullscreened video in
- New `content.windowed_fullscreen` setting to show e.g. a fullscreened video in
the window without fullscreening that window.
- New `:edit-command` command to edit the commandline in an editor.
- New `tabs.persist_mode_on_change` setting to keep the current mode when
switching tabs.
- New `session.lazy_restore` setting which allows to not load pages immediately
when restoring a session.
- New `hist_importer.py` script to import history from Firefox/Chromium.
- New `{protocol}` replacement for `tabs.title.format` and friends.
- New `-o` flag for `:spawn` to show stdout/stderr in a new tab.
- Support for incremental search, with a new `search.incremental` setting.
- New `--rapid` flag for `:command-accept` (bound to `Ctrl-Enter` by default),
which allows executing a command in the completion without closing it.
- The `colors.completion.fg` setting can now be a list, allowing to specify
different colors for the three completion columns.
Changed
~~~~~~~
- Some tabs settings got renamed:
- Some settings got renamed:
* `tabs.width.bar` -> `tabs.width`
* `tabs.width.indicator` -> `tabs.indicator.width`
* `tabs.indicator_padding` -> `tabs.indicator.padding`
* `session_default_name` -> `session.default_name`
* `ignore_case` -> `search.ignore_case`
- High-DPI favicons are now used when available.
- The `asciidoc2html.py` script now uses Pygments (which is already a dependency
of qutebrowser) instead of `source-highlight` for syntax highlighting.
@ -71,6 +91,25 @@ Changed
- The `qute://version` page now also shows the uptime of qutebrowser.
- qutebrowser now prompts to create a non-existing directory when starting a
download.
- Much improved user stylesheet handling which reduces flickering
and updates immediately after setting a stylesheet.
- `:completion-item-focus` now has a `--history` flag which causes it to go
through the command history when no text was entered. The default bindings for
cursor keys in the completion changed to use that, so that they can be used
again to navigate through completion items when a text was entered.
- `:debug-pyeval` now has a `--file` argument so it takes a filename instead of
a line of code.
- `:jseval --file` now searches relative paths in a js/ subdir in qutebrowser's
data dir, e.g. `~/.local/share/qutebrowser/js`.
- The current/default bindings are now shown in the :bind completion.
- Empty categories are now hidden in the `:open` completion.
- Search terms for URLs and titles can now be mixed when filtering the
completion.
- The default font size for the UI got bumped up from 8pt to 10pt.
- Improved matching in the completion: The words entered are now matched in any
order, and mixed matches on URL/tite are possible.
- The system's default encoding (rather than UTF-8) is now used to decode
subprocess output.
Fixed
~~~~~
@ -87,6 +126,14 @@ Fixed
- Fixed crash when opening `qute://help/img`
- Fixed `gU` (`:navigate up`) on `qute://help` and webservers not handling `..`
in a URL.
- Using e.g. `-s backend webkit` to set the backend now works correctly.
- Fixed crash when closing the tab an external editor was opened in.
- When using `:search-next` before a search is finished, no warning about no
results being found is shown anymore.
- Fix :click-element with an ID containing non-alphanumeric characters.
- Fix crash when a subprocess outputs data which is not decodable as UTF-8.
- Fix crash when closing a tab immediately after hinting.
- Worked around issues in Qt 5.10 with loading progress never being finished.
Deprecated
~~~~~~~~~~
@ -99,18 +146,24 @@ Removed
- The long-deprecated `:prompt-yes`, `:prompt-no`, `:paste-primary` and `:paste`
commands have been removed.
- The invocation `:download <url> <dest>` which was deprecated in v0.5.0 was
removed, use `:download --dest <dest> <url>` instead.
- The `messages.unfocused` option which wasn't used anymore was removed.
- The `x[xtb]` default bindings got removed again as many users accidentally
triggered them.
v1.0.4 (unreleased)
-------------------
v1.0.4
------
Fixed
~~~~~
- The `qute://gpl` page now works correctly again.
- Trying to bind an empty command now doesn't crash anymore.
- Fixed crash when `:config-write-py` fails to write to the given path.
- Fixed crash for some users when selecting a file with Qt 5.9.3
- Improved handling for various SQL errors
- Fix crash when setting content.cache.size to a big value (> 2 GB)
v1.0.3
------

View File

@ -100,16 +100,10 @@ Currently, the following tox environments are available:
- `py35`, `py36`: Run pytest for python 3.5/3.6 with the system-wide PyQt.
- `py36-pyqt57`, ..., `py36-pyqt59`: Run pytest with the given PyQt version (`py35-*` also works).
- `py36-pyqt59-cov`: Run with coverage support (other Python/PyQt versions work too).
* `flake8`: Run https://pypi.python.org/pypi/flake8[flake8] checks:
https://pypi.python.org/pypi/pyflakes[pyflakes],
https://pypi.python.org/pypi/pep8[pep8],
https://pypi.python.org/pypi/mccabe[mccabe].
* `flake8`: Run various linting checks via https://pypi.python.org/pypi/flake8[flake8].
* `vulture`: Run https://pypi.python.org/pypi/vulture[vulture] to find
unused code portions.
* `pylint`: Run http://pylint.org/[pylint] static code analysis.
* `pydocstyle`: Check
https://www.python.org/dev/peps/pep-0257/[PEP257] compliance with
https://github.com/PyCQA/pydocstyle[pydocstyle].
* `pyroma`: Check packaging practices with
https://pypi.python.org/pypi/pyroma/[pyroma].
* `eslint`: Run http://eslint.org/[ESLint] javascript checker.

View File

@ -221,5 +221,5 @@ My issue is not listed.::
https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or
using the `:report` command.
If you are reporting a segfault, make sure you read the
link:doc/stacktrace.asciidoc[guide] on how to report them with all needed
link:stacktrace.asciidoc[guide] on how to report them with all needed
information.

View File

@ -47,12 +47,14 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<download-open,download-open>>|Open the last/[count]th download.
|<<download-remove,download-remove>>|Remove the last/[count]th download from the list.
|<<download-retry,download-retry>>|Retry the first failed/[count]th download.
|<<edit-command,edit-command>>|Open an editor to modify the current command.
|<<edit-url,edit-url>>|Navigate to a url formed in an external editor.
|<<enter-mode,enter-mode>>|Enter a key mode.
|<<fake-key,fake-key>>|Send a fake keypress or key string to the website or qutebrowser.
|<<follow-selected,follow-selected>>|Follow the selected text.
|<<forward,forward>>|Go forward in the history of the current tab.
|<<fullscreen,fullscreen>>|Toggle fullscreen mode.
|<<greasemonkey-reload,greasemonkey-reload>>|Re-read Greasemonkey scripts from disk.
|<<help,help>>|Show help about a command or setting.
|<<hint,hint>>|Start hinting.
|<<history,history>>|Show browsing history.
@ -332,12 +334,10 @@ Write the current configuration to a config.py file.
[[download]]
=== download
Syntax: +:download [*--mhtml*] [*--dest* 'dest'] ['url'] ['dest-old']+
Syntax: +:download [*--mhtml*] [*--dest* 'dest'] ['url']+
Download a given URL, or current page if no URL given.
The form `:download [url] [dest]` is deprecated, use `:download --dest [dest] [url]` instead.
==== positional arguments
* +'url'+: The URL to download. If not given, download the current page.
@ -407,6 +407,15 @@ Retry the first failed/[count]th download.
==== count
The index of the download to retry.
[[edit-command]]
=== edit-command
Syntax: +:edit-command [*--run*]+
Open an editor to modify the current command.
==== optional arguments
* +*-r*+, +*--run*+: Run the command if the editor exits successfully.
[[edit-url]]
=== edit-url
Syntax: +:edit-url [*--bg*] [*--tab*] [*--window*] [*--private*] [*--related*] ['url']+
@ -481,6 +490,12 @@ Toggle fullscreen mode.
==== optional arguments
* +*-l*+, +*--leave*+: Only leave fullscreen if it was entered by the page.
[[greasemonkey-reload]]
=== greasemonkey-reload
Re-read Greasemonkey scripts from disk.
The scripts are read from a 'greasemonkey' subdirectory in qutebrowser's data directory (see `:version`).
[[help]]
=== help
Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
@ -501,7 +516,7 @@ Show help about a command or setting.
[[hint]]
=== hint
Syntax: +:hint [*--rapid*] [*--mode* 'mode'] [*--add-history*]
Syntax: +:hint [*--mode* 'mode'] [*--add-history*] [*--rapid*]
['group'] ['target'] ['args' ['args' ...]]+
Start hinting.
@ -555,11 +570,6 @@ Start hinting.
==== optional arguments
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. With rapid hinting, the hint mode isn't left after a hint is followed, so you can easily
open multiple links. This is only possible with targets
`tab` (with `tabs.background_tabs=true`), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`.
* +*-m*+, +*--mode*+: The hinting mode to use.
- `number`: Use numeric hints.
@ -571,6 +581,11 @@ Start hinting.
* +*-a*+, +*--add-history*+: Whether to add the spawned or yanked link to the browsing history.
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. With rapid hinting, the hint mode isn't left after a hint is followed, so you can easily
open multiple links. This is only possible with targets
`tab` (with `tabs.background_tabs=true`), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
@ -629,7 +644,10 @@ Evaluate a JavaScript string.
* +'js-code'+: The string/file to evaluate.
==== optional arguments
* +*-f*+, +*--file*+: Interpret js-code as a path to a file.
* +*-f*+, +*--file*+: Interpret js-code as a path to a file. If the path is relative, the file is searched in a js/ subdir
in qutebrowser's data dir, e.g.
`~/.local/share/qutebrowser/js`.
* +*-q*+, +*--quiet*+: Don't show resulting JS object.
* +*-w*+, +*--world*+: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in.
@ -1070,7 +1088,7 @@ Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-win
Save a session.
==== positional arguments
* +'name'+: The name of the session. If not given, the session configured in session_default_name is saved.
* +'name'+: The name of the session. If not given, the session configured in session.default_name is saved.
==== optional arguments
@ -1128,7 +1146,7 @@ Set a mark at the current scroll position in the current tab.
[[spawn]]
=== spawn
Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+
Syntax: +:spawn [*--userscript*] [*--verbose*] [*--output*] [*--detach*] 'cmdline'+
Spawn a command in a shell.
@ -1143,6 +1161,7 @@ Spawn a command in a shell.
- `/usr/share/qutebrowser/userscripts`
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
* +*-o*+, +*--output*+: Whether the output should be shown in a new tab.
* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
==== note
@ -1402,8 +1421,13 @@ How many steps to zoom out.
|==============
[[command-accept]]
=== command-accept
Syntax: +:command-accept [*--rapid*]+
Execute the command currently in the commandline.
==== optional arguments
* +*-r*+, +*--rapid*+: Run the command without closing or clearing the command bar.
[[command-history-next]]
=== command-history-next
Go forward in the commandline history.
@ -1418,13 +1442,16 @@ Delete the current completion item.
[[completion-item-focus]]
=== completion-item-focus
Syntax: +:completion-item-focus 'which'+
Syntax: +:completion-item-focus [*--history*] 'which'+
Shift the focus of the completion menu to another item.
==== positional arguments
* +'which'+: 'next', 'prev', 'next-category', or 'prev-category'.
==== optional arguments
* +*-H*+, +*--history*+: Navigate through command history if no text was typed.
[[completion-item-yank]]
=== completion-item-yank
Syntax: +:completion-item-yank [*--sel*]+
@ -1773,7 +1800,7 @@ Change the log level for console logging.
[[debug-pyeval]]
=== debug-pyeval
Syntax: +:debug-pyeval [*--quiet*] 's'+
Syntax: +:debug-pyeval [*--file*] [*--quiet*] 's'+
Evaluate a python string and display the results as a web page.
@ -1781,6 +1808,7 @@ Evaluate a python string and display the results as a web page.
* +'s'+: The string to evaluate.
==== optional arguments
* +*-f*+, +*--file*+: Interpret s as a path to file, also implies --quiet.
* +*-q*+, +*--quiet*+: Don't show the output in a new tab.
==== note

View File

@ -17,15 +17,24 @@ did in your old configuration, compared to the old defaults.
Other changes in default settings:
- `<Up>` and `<Down>` in the completion now navigate through command history
instead of selecting completion items. Use `<Tab>`/`<Shift-Tab>` to cycle
through the completion instead.
- In v1.1.x and newer, `<Up>` and `<Down>` navigate through command history
if no text was entered yet.
With v1.0.x, they always navigate through command history instead of selecting
completion items. Use `<Tab>`/`<Shift-Tab>` to cycle through the completion
instead.
You can get back the old behavior by doing:
+
----
:bind -m command <Up> completion-item-focus prev
:bind -m command <Down> completion-item-focus next
----
+
or always navigate through command history with
+
----
:bind -m command <Up> command-history-prev
:bind -m command <Down> command-history-next
----
- The default for `completion.web_history_max_items` is now set to `-1`, showing
an unlimited number of items in the completion for `:open` as the new
@ -255,7 +264,7 @@ get a string:
.config.py:
[source,python]
----
print(str(config.configdir / 'config.py')
print(str(config.configdir / 'config.py'))
----
Handling errors

View File

@ -199,7 +199,6 @@
|<<hints.scatter,hints.scatter>>|Scatter hint key chains (like Vimium) or not (like dwb).
|<<hints.uppercase,hints.uppercase>>|Make characters in hint strings uppercase.
|<<history_gap_interval,history_gap_interval>>|Maximum time (in minutes) between two history items for them to be considered being from the same browsing session.
|<<ignore_case,ignore_case>>|When to find text on a page case-insensitively.
|<<input.forward_unbound_keys,input.forward_unbound_keys>>|Which unbound keys to forward to the webview in normal mode.
|<<input.insert_mode.auto_leave,input.insert_mode.auto_leave>>|Leave insert mode if a non-editable element is clicked.
|<<input.insert_mode.auto_load,input.insert_mode.auto_load>>|Automatically enter insert mode if an editable element is focused after loading the page.
@ -222,7 +221,10 @@
|<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling.
|<<scrolling.bar,scrolling.bar>>|Show a scrollbar.
|<<scrolling.smooth,scrolling.smooth>>|Enable smooth scrolling for web pages.
|<<session_default_name,session_default_name>>|Name of the session to save by default.
|<<search.ignore_case,search.ignore_case>>|When to find text on a page case-insensitively.
|<<search.incremental,search.incremental>>|Find text on a page incrementally, renewing the search for each typed character.
|<<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.
@ -239,6 +241,7 @@
|<<tabs.new_position.related,tabs.new_position.related>>|Position of new tabs opened from another tab.
|<<tabs.new_position.unrelated,tabs.new_position.unrelated>>|Position of new tabs which aren't opened from another tab.
|<<tabs.padding,tabs.padding>>|Padding (in pixels) around text for tabs.
|<<tabs.persist_mode_on_change,tabs.persist_mode_on_change>>|Stay in insert/passthrough mode when switching tabs.
|<<tabs.pinned.shrink,tabs.pinned.shrink>>|Shrink pinned tabs down to their contents.
|<<tabs.position,tabs.position>>|Position of the tab bar.
|<<tabs.select_on_remove,tabs.select_on_remove>>|Which tab to select when the focused tab is removed.
@ -424,19 +427,20 @@ Default:
* +pass:[&lt;Ctrl-K&gt;]+: +pass:[rl-kill-line]+
* +pass:[&lt;Ctrl-N&gt;]+: +pass:[command-history-next]+
* +pass:[&lt;Ctrl-P&gt;]+: +pass:[command-history-prev]+
* +pass:[&lt;Ctrl-Return&gt;]+: +pass:[command-accept --rapid]+
* +pass:[&lt;Ctrl-Shift-C&gt;]+: +pass:[completion-item-yank --sel]+
* +pass:[&lt;Ctrl-Shift-Tab&gt;]+: +pass:[completion-item-focus prev-category]+
* +pass:[&lt;Ctrl-Tab&gt;]+: +pass:[completion-item-focus next-category]+
* +pass:[&lt;Ctrl-U&gt;]+: +pass:[rl-unix-line-discard]+
* +pass:[&lt;Ctrl-W&gt;]+: +pass:[rl-unix-word-rubout]+
* +pass:[&lt;Ctrl-Y&gt;]+: +pass:[rl-yank]+
* +pass:[&lt;Down&gt;]+: +pass:[command-history-next]+
* +pass:[&lt;Down&gt;]+: +pass:[completion-item-focus --history next]+
* +pass:[&lt;Escape&gt;]+: +pass:[leave-mode]+
* +pass:[&lt;Return&gt;]+: +pass:[command-accept]+
* +pass:[&lt;Shift-Delete&gt;]+: +pass:[completion-item-del]+
* +pass:[&lt;Shift-Tab&gt;]+: +pass:[completion-item-focus prev]+
* +pass:[&lt;Tab&gt;]+: +pass:[completion-item-focus next]+
* +pass:[&lt;Up&gt;]+: +pass:[command-history-prev]+
* +pass:[&lt;Up&gt;]+: +pass:[completion-item-focus --history prev]+
- +pass:[hint]+:
* +pass:[&lt;Ctrl-B&gt;]+: +pass:[hint all tab-bg]+
@ -697,10 +701,15 @@ Default: +pass:[#333333]+
[[colors.completion.fg]]
=== colors.completion.fg
Text color of the completion widget.
May be a single color to use for all columns or a list of three colors, one for each column.
Type: <<types,QtColor>>
Type: <<types,List of QtColor&#44; or QtColor>>
Default: +pass:[white]+
Default:
- +pass:[white]+
- +pass:[white]+
- +pass:[white]+
[[colors.completion.item.selected.bg]]
=== colors.completion.item.selected.bg
@ -1455,6 +1464,7 @@ This setting is only available with the QtWebKit backend.
[[content.cache.size]]
=== content.cache.size
Size (in bytes) of the HTTP network cache. Null to use the default value.
With QtWebEngine, the maximum supported value is 2147483647 (~2 GB).
Type: <<types,Int>>
@ -2323,20 +2333,6 @@ Type: <<types,Int>>
Default: +pass:[30]+
[[ignore_case]]
=== ignore_case
When to find text on a page case-insensitively.
Type: <<types,String>>
Valid values:
* +always+: Search case-insensitively.
* +never+: Search case-sensitively.
* +smart+: Search case-sensitively if there are capital characters.
Default: +pass:[smart]+
[[input.forward_unbound_keys]]
=== input.forward_unbound_keys
Which unbound keys to forward to the webview in normal mode.
@ -2554,8 +2550,30 @@ Type: <<types,Bool>>
Default: +pass:[false]+
[[session_default_name]]
=== session_default_name
[[search.ignore_case]]
=== search.ignore_case
When to find text on a page case-insensitively.
Type: <<types,String>>
Valid values:
* +always+: Search case-insensitively.
* +never+: Search case-sensitively.
* +smart+: Search case-sensitively if there are capital characters.
Default: +pass:[smart]+
[[search.incremental]]
=== search.incremental
Find text on a page incrementally, renewing the search for each typed character.
Type: <<types,Bool>>
Default: +pass:[true]+
[[session.default_name]]
=== session.default_name
Name of the session to save by default.
If this is set to null, the session which was last loaded is saved.
@ -2563,6 +2581,14 @@ Type: <<types,SessionName>>
Default: empty
[[session.lazy_restore]]
=== session.lazy_restore
Load a restored tab as soon as it takes focus.
Type: <<types,Bool>>
Default: +pass:[false]+
[[spellcheck.languages]]
=== spellcheck.languages
Languages to use for spell checking.
@ -2798,6 +2824,14 @@ Default:
- +pass:[right]+: +pass:[5]+
- +pass:[top]+: +pass:[0]+
[[tabs.persist_mode_on_change]]
=== tabs.persist_mode_on_change
Stay in insert/passthrough mode when switching tabs.
Type: <<types,Bool>>
Default: +pass:[false]+
[[tabs.pinned.shrink]]
=== tabs.pinned.shrink
Shrink pinned tabs down to their contents.
@ -2894,8 +2928,9 @@ The following placeholders are defined:
* `{scroll_pos}`: Page scroll position.
* `{host}`: Host of the current web page.
* `{backend}`: Either ''webkit'' or ''webengine''
* `{private}` : Indicates when private mode is enabled.
* `{current_url}` : URL of the current web page.
* `{private}`: Indicates when private mode is enabled.
* `{current_url}`: URL of the current web page.
* `{protocol}`: Protocol (http/https/...) of the current web page.
Type: <<types,FormatString>>

View File

@ -21,7 +21,7 @@ Those distributions only have Python 3.4 and a too old Qt version available,
while qutebrowser requires Python 3.5 and Qt 5.7.1 or newer.
It should be possible to install Python 3.5 e.g. from the
https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or via_ipca
https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or via
https://github.com/pyenv/pyenv[pyenv], but nobody tried that yet.
If you get qutebrowser running on those distributions, please
@ -35,7 +35,7 @@ Ubuntu 16.04 doesn't come with an up-to-date engine (a new enough QtWebKit, or
QtWebEngine). However, it comes with Python 3.5, so you can
<<tox,install qutebrowser via tox>>.
Debian Stretch / Ubuntu 17.04 and newer
Debian Stretch / Ubuntu 17.04 and 17.10
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Those versions come with QtWebEngine in the repositories. This makes it possible
@ -54,7 +54,18 @@ Install the packages:
# apt install ./qutebrowser_*_all.deb
----
Some additional hints:
Debian Testing / Ubuntu 18.04
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
On Debian Testing, qutebrowser is in the official repositories, and you can
install it with apt:
----
# apt install qutebrowser
----
Additional hints
~~~~~~~~~~~~~~~~
- Alternatively, you can <<tox,install qutebrowser via tox>> to get a newer
QtWebEngine version.
@ -67,8 +78,7 @@ $ python3 scripts/asciidoc2html.py
----
- If you prefer using QtWebKit, there's an up-to-date version available in
Debian experimental, or from http://repo.paretje.be/unstable/[this repository]
for Debian Stretch.
https://packages.debian.org/buster/libqt5webkit5[Debian Testing].
- If video or sound don't work with QtWebKit, try installing the gstreamer plugins:
+
----
@ -78,10 +88,29 @@ $ python3 scripts/asciidoc2html.py
On Fedora
---------
The Fedora packages are lagging behind a lot and are currently effectively
unmaintained. It's recommended to <<tox,install qutebrowser via tox>> instead.
NOTE: Fedora's packages used to be outdated for a long time, but are
now (November 2017) maintained and up-to-date again.
Related Fedora bug: https://bugzilla.redhat.com/show_bug.cgi?id=1467748[1467748]
qutebrowser is available in the official repositories:
-----
# dnf install qutebrowser
-----
However, note that Fedora 25/26 won't be updated to qutebrowser v1.0, so you
might want to <<tox,install qutebrowser via tox>> instead there.
Additional hints
~~~~~~~~~~~~~~~~
Fedora only ships free software in the repositories.
To be able to play videos with proprietary codecs with QtWebEngine, you will
need to install an additional package from the RPM Fusion Free repository.
For more information see https://rpmfusion.org/Configuration.
-----
# dnf install qt5-qtwebengine-freeworld
-----
On Archlinux
------------
@ -182,6 +211,10 @@ To use the QtWebEngine backend, install `libqt5-qtwebengine`.
On OpenBSD
----------
WARNING: OpenBSD only packages a legacy unmaintained version of QtWebKit (for
which support was dropped in qutebrowser v1.0). It's advised to not use
qutebrowser from OpenBSD ports for untrusted websites.
qutebrowser is in http://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/www/qutebrowser/[OpenBSD ports].
Install the package:
@ -197,6 +230,21 @@ Or alternatively, use the ports system :
# make install
----
On FreeBSD
----------
qutebrowser is in https://www.freshports.org/www/qutebrowser/[FreeBSD ports].
It can be installed with:
----
# cd /usr/ports/www/qutebrowser
# make install clean
----
At present, precompiled packages are not available for this port,
and QtWebEngine backend is also not available.
On Windows
----------
@ -353,8 +401,8 @@ local Qt install instead of installing PyQt in the virtualenv. However, unless
you have a new QtWebKit or QtWebEngine available, qutebrowser will not work. It
also typically means you'll be using an older release of QtWebEngine.
On Windows, run `tox -e 'mkvenv-win' instead, however make sure that ONLY
Python3 is in your PATH before running tox.
On Windows, run `set PYTHON=C:\path\to\python.exe` (CMD) or ``$Env:PYTHON =
"..."` (Powershell) first.
Creating a wrapper script
~~~~~~~~~~~~~~~~~~~~~~~~~

25
misc/Makefile Normal file
View File

@ -0,0 +1,25 @@
PYTHON = python3
DESTDIR = /
ICONSIZES = 16 24 32 48 64 128 256 512
.PHONY: install
doc/qutebrowser.1.html:
a2x -f manpage doc/qutebrowser.1.asciidoc
install: doc/qutebrowser.1.html
$(PYTHON) setup.py install --root="$(DESTDIR)" --optimize=1
install -Dm644 doc/qutebrowser.1 \
"$(DESTDIR)/usr/share/man/man1/qutebrowser.1"
install -Dm644 misc/qutebrowser.desktop \
"$(DESTDIR)/usr/share/applications/qutebrowser.desktop"
$(foreach i,$(ICONSIZES),install -Dm644 "icons/qutebrowser-$(i)x$(i).png" \
"$(DESTDIR)/usr/share/icons/hicolor/$(i)x$(i)/apps/qutebrowser.png";)
install -Dm644 icons/qutebrowser.svg \
"$(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qutebrowser.svg"
install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/userscripts/" \
$(wildcard misc/userscripts/*)
install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/scripts/" \
$(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \
scripts/testbrowser_cpp scripts/asciidoc2html.py scripts/setupcommon.py \
scripts/link_pyqt.py,$(wildcard scripts/*))

View File

@ -7,5 +7,5 @@ Categories=Network;WebBrowser;
Exec=qutebrowser %u
Terminal=false
StartupNotify=false
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/qute;
Keywords=Browser

View File

@ -1,5 +1,5 @@
This directory contains various `requirements` files which are used by `tox` to
have reproducable tests with pinned versions.
have reproducible tests with pinned versions.
The files are generated based on unpinned requirements in `*.txt-raw` files.

View File

@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
check-manifest==0.35
check-manifest==0.36

View File

@ -2,7 +2,7 @@
certifi==2017.11.5
chardet==3.0.4
codecov==2.0.9
codecov==2.0.10
coverage==4.4.2
idna==2.6
requests==2.18.4

View File

@ -1,23 +1,25 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
flake8==2.6.2 # rq.filter: < 3.0.0
attrs==17.3.0
flake8==3.5.0
flake8-bugbear==17.12.0
flake8-builtins==1.0.post0
flake8-comprehensions==1.4.1
flake8-copyright==0.2.0
flake8-debugger==3.0.0
flake8-deprecated==1.2.1 # rq.filter: < 1.3
flake8-docstrings==1.0.3 # rq.filter: < 1.1.0
flake8-deprecated==1.3
flake8-docstrings==1.1.0
flake8-future-import==0.4.3
flake8-mock==0.3
flake8-pep3101==1.0 # rq.filter: < 1.1
flake8-per-file-ignores==0.4
flake8-polyfill==1.0.1
flake8-putty==0.4.0
flake8-string-format==0.2.3
flake8-tidy-imports==1.1.0
flake8-tuple==0.2.13
mccabe==0.6.1
packaging==16.8
pep8-naming==0.4.1
pycodestyle==2.3.1
pydocstyle==1.1.1 # rq.filter: < 2.0.0
pydocstyle==2.1.1
pyflakes==1.6.0
pyparsing==2.2.0
six==1.11.0
snowballstemmer==1.2.1

View File

@ -1,27 +1,17 @@
flake8<3.0.0
flake8
flake8-bugbear
flake8-builtins
flake8-comprehensions
flake8-copyright
flake8-debugger
flake8-deprecated<1.3
flake8-docstrings<1.1.0
flake8-deprecated
flake8-docstrings
flake8-future-import
flake8-mock
flake8-pep3101<1.1
flake8-putty
flake8-per-file-ignores
flake8-string-format
flake8-tidy-imports
flake8-tuple
pep8-naming
pydocstyle<2.0.0
pydocstyle
pyflakes
# Pinned to 2.0.0 otherwise
pycodestyle==2.3.1
# Pinned to 0.5.3 otherwise
mccabe==0.6.1
# Waiting until flake8-putty updated
#@ filter: flake8 < 3.0.0
#@ filter: pydocstyle < 2.0.0
#@ filter: flake8-docstrings < 1.1.0
#@ filter: flake8-pep3101 < 1.1
#@ filter: flake8-deprecated < 1.3

View File

@ -3,6 +3,6 @@
appdirs==1.4.3
packaging==16.8
pyparsing==2.2.0
setuptools==36.7.1
setuptools==38.2.5
six==1.11.0
wheel==0.30.0

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
altgraph==0.14
altgraph==0.15
future==0.16.0
macholib==1.8
macholib==1.9
pefile==2017.11.5
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller

View File

@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==1.5.3
astroid==1.6.0
certifi==2017.11.5
chardet==3.0.4
github3.py==0.9.6
@ -8,7 +8,7 @@ idna==2.6
isort==4.2.15
lazy-object-proxy==1.3.1
mccabe==0.6.1
pylint==1.7.4
pylint==1.8.1
./scripts/dev/pylint_checkers
requests==2.18.4
six==1.11.0

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.9 # rq.filter: != 5.9.1
sip==4.19.5
PyQt5==5.9.2
sip==4.19.6

View File

@ -1,3 +1 @@
#@ filter: PyQt5 != 5.9.1
PyQt5==5.9
PyQt5

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.14
pyroma==2.2
pyroma==2.3

View File

@ -12,12 +12,6 @@ git+https://github.com/jenisys/parse_type.git
hg+https://bitbucket.org/pytest-dev/py
git+https://github.com/pytest-dev/pytest.git@features
git+https://github.com/pytest-dev/pytest-bdd.git
# This is broken at the moment because logfail tries to access
# LogCaptureHandler
# git+https://github.com/eisensheng/pytest-catchlog.git
pytest-catchlog==1.2.2
git+https://github.com/pytest-dev/pytest-cov.git
git+https://github.com/pytest-dev/pytest-faulthandler.git
git+https://github.com/pytest-dev/pytest-instafail.git

View File

@ -2,7 +2,7 @@
attrs==17.3.0
beautifulsoup4==4.6.0
cheroot==5.8.3
cheroot==6.0.0
click==6.7
# colorama==0.3.9
coverage==4.4.2
@ -10,30 +10,30 @@ EasyProcess==0.2.3
fields==5.0.0
Flask==0.12.2
glob2==0.6
hunter==2.0.1
hypothesis==3.37.0
hunter==2.0.2
hypothesis==3.44.4
itsdangerous==0.24
# Jinja2==2.9.6
# Jinja2==2.10
Mako==1.0.7
# MarkupSafe==1.0
parse==1.8.2
parse-type==0.4.2
py==1.4.34
pluggy==0.6.0
py==1.5.2
py-cpuinfo==3.3.0
pytest==3.2.3
pytest==3.3.1
pytest-bdd==2.19.0
pytest-benchmark==3.1.1
pytest-catchlog==1.2.2
pytest-cov==2.5.1
pytest-faulthandler==1.3.1
pytest-instafail==0.3.0
pytest-mock==1.6.3
pytest-qt==2.2.1
pytest-qt==2.3.0
pytest-repeat==0.4.1
pytest-rerunfailures==3.1
pytest-travis-fold==1.2.0
pytest-rerunfailures==4.0
pytest-travis-fold==1.3.0
pytest-xvfb==1.0.0
PyVirtualDisplay==0.2.1
six==1.11.0
vulture==0.26
Werkzeug==0.12.2
Werkzeug==0.13

View File

@ -7,7 +7,6 @@ hypothesis
pytest
pytest-bdd
pytest-benchmark
pytest-catchlog
pytest-cov
pytest-faulthandler
pytest-instafail

View File

@ -1,6 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
pluggy==0.5.2
py==1.4.34
pluggy==0.6.0
py==1.5.2
six==1.11.0
tox==2.9.1
virtualenv==15.1.0

View File

@ -144,7 +144,7 @@ fi
pkill -f "${program_}"
# start youtube download in stream mode (-o -) into temporary file
youtube-dl -qo - "$1" > ${file_to_cast} &
youtube-dl -qo - "$1" > "${file_to_cast}" &
ytdl_pid=$!
msg info "Casting $1" >> "$QUTE_FIFO"
@ -153,4 +153,4 @@ tail -F "${file_to_cast}" | ${program_} -
# cleanup remaining background process and file on disk
kill ${ytdl_pid}
rm -rf ${tmpdir}
rm -rf "${tmpdir}"

View File

@ -41,7 +41,7 @@
[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com'
url=$(echo "$QUTE_URL" | cat - "$QUTE_CONFIG_DIR/quickmarks" "$QUTE_DATA_DIR/history" | dmenu -l 15 -p qutebrowser)
url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo "$url")
url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | grep -E "https?:" || echo "$url")
[ -z "${url// }" ] && exit

View File

@ -1,4 +1,5 @@
#!/bin/sh
set -euo pipefail
#
# Behavior:
# Userscript for qutebrowser which will take the raw JSON text of the current
@ -19,29 +20,23 @@
#
# Bryan Gilbert, 2017
# do not run pygmentize on files larger than this amount of bytes
MAX_SIZE_PRETTIFY=10485760 # 10 MB
# default style to monokai if none is provided
STYLE=${1:-monokai}
# format json using jq
FORMATTED_JSON="$(cat "$QUTE_TEXT" | jq '.')"
# if jq command failed or formatted json is empty, assume failure and terminate
if [ $? -ne 0 ] || [ -z "$FORMATTED_JSON" ]; then
echo "Invalid json, aborting..."
exit 1
TEMP_FILE="$(mktemp)"
jq . "$QUTE_TEXT" >"$TEMP_FILE"
# try GNU stat first and then OSX stat if the former fails
FILE_SIZE=$(
stat --printf="%s" "$TEMP_FILE" 2>/dev/null ||
stat -f%z "$TEMP_FILE" 2>/dev/null
)
if [ "$FILE_SIZE" -lt "$MAX_SIZE_PRETTIFY" ]; then
pygmentize -l json -f html -O full,style="$STYLE" <"$TEMP_FILE" >"${TEMP_FILE}_"
mv -f "${TEMP_FILE}_" "$TEMP_FILE"
fi
# calculate the filesize of the json document
FILE_SIZE=$(ls -s --block-size=1048576 "$QUTE_TEXT" | cut -d' ' -f1)
# use pygments to pretty-up the json (syntax highlight) if file is less than 10MB
if [ "$FILE_SIZE" -lt "10" ]; then
FORMATTED_JSON="$(echo "$FORMATTED_JSON" | pygmentize -l json -f html -O full,style=$STYLE)"
fi
# create a temp file and write the formatted json to that file
TEMP_FILE="$(mktemp --suffix '.html')"
echo "$FORMATTED_JSON" > $TEMP_FILE
# send the command to qutebrowser to open the new file containing the formatted json
echo "open -t file://$TEMP_FILE" >> "$QUTE_FIFO"

View File

@ -76,6 +76,7 @@ crop-first-column() {
ls-files() {
# add the slash at the end of the download dir enforces to follow the
# symlink, if the DOWNLOAD_DIR itself is a symlink
# shellcheck disable=SC2010
ls -Q --quoting-style escape -h -o -1 -A -t "${DOWNLOAD_DIR}/" \
| grep '^[-]' \
| cut -d' ' -f3- \
@ -91,10 +92,10 @@ if [ "${#entries[@]}" -eq 0 ] ; then
die "Download directory »${DOWNLOAD_DIR}« empty"
fi
line=$(printf "%s\n" "${entries[@]}" \
line=$(printf '%s\n' "${entries[@]}" \
| crop-first-column 55 \
| column -s $'\t' -t \
| $ROFI_CMD "${rofi_default_args[@]}" $ROFI_ARGS) || true
| $ROFI_CMD "${rofi_default_args[@]}" "$ROFI_ARGS") || true
if [ -z "$line" ]; then
exit 0
fi

View File

@ -64,7 +64,7 @@ die() {
javascript_escape() {
# print the first argument in an escaped way, such that it can safely
# be used within javascripts double quotes
sed "s,[\\\'\"],\\\&,g" <<< "$1"
sed "s,[\\\\'\"],\\\\&,g" <<< "$1"
}
# ======================================================= #
@ -178,7 +178,7 @@ choose_entry_menu() {
if [ "$nr" -eq 1 ] && ! ((menu_if_one_entry)) ; then
file="${files[0]}"
else
file=$( printf "%s\n" "${files[@]}" | "${MENU_COMMAND[@]}" )
file=$( printf '%s\n' "${files[@]}" | "${MENU_COMMAND[@]}" )
fi
}
@ -236,7 +236,7 @@ pass_backend() {
if ((match_line)) ; then
# add entries with matching URL-tag
while read -r -d "" passfile ; do
if $GPG "${GPG_OPTS}" -d "$passfile" \
if $GPG "${GPG_OPTS[@]}" -d "$passfile" \
| grep --max-count=1 -iE "${match_line_pattern}${url}" > /dev/null
then
passfile="${passfile#$PREFIX}"
@ -269,7 +269,7 @@ pass_backend() {
break
fi
fi
done < <($GPG "${GPG_OPTS}" -d "$path" )
done < <($GPG "${GPG_OPTS[@]}" -d "$path" )
}
}
# =======================================================
@ -283,8 +283,8 @@ secret_backend() {
query_entries() {
local domain="$1"
while read -r line ; do
if [[ "$line" =~ "attribute.username = " ]] ; then
files+=("$domain ${line#${BASH_REMATCH[0]}}")
if [[ "$line" == "attribute.username = "* ]] ; then
files+=("$domain ${line:21}")
fi
done < <( secret-tool search --unlock --all domain "$domain" 2>&1 )
}
@ -303,6 +303,7 @@ pass_backend
QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qutebrowser/}
PWFILL_CONFIG=${PWFILL_CONFIG:-${QUTE_CONFIG_DIR}/password_fill_rc}
if [ -f "$PWFILL_CONFIG" ] ; then
# shellcheck source=/dev/null
source "$PWFILL_CONFIG"
fi
init
@ -311,7 +312,7 @@ simplify_url "$QUTE_URL"
query_entries "${simple_url}"
no_entries_found
# remove duplicates
mapfile -t files < <(printf "%s\n" "${files[@]}" | sort | uniq )
mapfile -t files < <(printf '%s\n' "${files[@]}" | sort | uniq )
choose_entry
if [ -z "$file" ] ; then
# choose_entry didn't want any of these entries

View File

@ -17,20 +17,21 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""
Insert login information using pass and a dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...). A short
demonstration can be seen here: https://i.imgur.com/KN3XuZP.gif.
"""
# Insert login information using pass and a dmenu-provider (e.g. dmenu, rofi -dmenu, ...).
# A short demonstration can be seen here: https://i.imgur.com/KN3XuZP.gif.
#
# The domain of the site has to appear as a segment in the pass path, for example: "github.com/cryzed" or
# "websites/github.com". How the username and password are determined is freely configurable using the CLI arguments.
# The login information is inserted by emulating key events using qutebrowser's fake-key command in this manner:
# [USERNAME]<Tab>[PASSWORD], which is compatible with almost all login forms.
#
# Dependencies: tldextract (Python 3 module), pass
# For issues and feedback please use: https://github.com/cryzed/qutebrowser-userscripts.
#
# WARNING: The login details are viewable as plaintext in qutebrowser's debug log (qute://log) and might be shared if
# you decide to submit a crash report!
USAGE = """The domain of the site has to appear as a segment in the pass path, for example: "github.com/cryzed" or
"websites/github.com". How the username and password are determined is freely configurable using the CLI arguments. The
login information is inserted by emulating key events using qutebrowser's fake-key command in this manner:
[USERNAME]<Tab>[PASSWORD], which is compatible with almost all login forms."""
EPILOG = """Dependencies: tldextract (Python 3 module), pass.
For issues and feedback please use: https://github.com/cryzed/qutebrowser-userscripts.
WARNING: The login details are viewable as plaintext in qutebrowser's debug log (qute://log) and might be shared if
you decide to submit a crash report!"""
import argparse
import enum
@ -44,8 +45,8 @@ import sys
import tldextract
argument_parser = argparse.ArgumentParser()
argument_parser.add_argument('url', nargs='?', default=os.environ['QUTE_URL'])
argument_parser = argparse.ArgumentParser(description=__doc__, usage=USAGE, epilog=EPILOG)
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
argument_parser.add_argument('--password-store', '-p', default=os.path.expanduser('~/.password-store'),
help='Path to your pass password-store')
argument_parser.add_argument('--username-pattern', '-u', default=r'.*/(.+)',
@ -71,6 +72,7 @@ stderr = functools.partial(print, file=sys.stderr)
class ExitCodes(enum.IntEnum):
SUCCESS = 0
FAILURE = 1
# 1 is automatically used if Python throws an exception
NO_PASS_CANDIDATES = 2
COULD_NOT_MATCH_USERNAME = 3
@ -108,6 +110,10 @@ def dmenu(items, invocation, encoding):
def main(arguments):
if not arguments.url:
argument_parser.print_help()
return ExitCodes.FAILURE
extract_result = tldextract.extract(arguments.url)
# Expand potential ~ in paths, since this script won't be called from a shell that does it for us

View File

@ -35,17 +35,12 @@ get_selection() {
# Main
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/font
if [[ -s $confdir/dmenu/font ]]; then
read -r font < "$confdir"/dmenu/font
fi
[[ -s $confdir/dmenu/font ]] && read -r font < "$confdir"/dmenu/font
if [[ $font ]]; then
opts+=(-fn "$font")
fi
[[ $font ]] && opts+=(-fn "$font")
if [[ -s $optsfile ]]; then
source "$optsfile"
fi
# shellcheck source=/dev/null
[[ -s $optsfile ]] && source "$optsfile"
url=$(get_selection)
url=${url/*http/http}

View File

@ -32,7 +32,7 @@ add_feed () {
if grep -Fq "$1" "feeds"; then
notice "$1 is saved already."
else
printf "%s\n" "$1" >> "feeds"
printf '%s\n' "$1" >> "feeds"
fi
}
@ -57,7 +57,7 @@ notice () {
# Update a database of a feed and open new URLs
read_items () {
cd read_urls
cd read_urls || return 1
feed_file="$(echo "$1" | tr -d /)"
feed_temp_file="$(mktemp "$feed_file.tmp.XXXXXXXXXX")"
feed_new_items="$(mktemp "$feed_file.new.XXXXXXXXXX")"
@ -75,7 +75,7 @@ read_items () {
cat "$feed_new_items" >> "$feed_file"
sort -o "$feed_file" "$feed_file"
rm "$feed_temp_file" "$feed_new_items"
fi | while read item; do
fi | while read -r item; do
echo "open -t $item" > "$QUTE_FIFO"
done
}
@ -85,7 +85,7 @@ if [ ! -d "$config_dir/read_urls" ]; then
mkdir -p "$config_dir/read_urls"
fi
cd "$config_dir"
cd "$config_dir" || exit 1
if [ $# != 0 ]; then
for arg in "$@"; do
@ -115,7 +115,7 @@ if < /dev/null grep --help 2>&1 | grep -q -- -a; then
text_only="-a"
fi
while read feed_url; do
while read -r feed_url; do
read_items "$feed_url" &
done < "$config_dir/feeds"

View File

@ -25,12 +25,10 @@
[[ $QUTE_MODE == 'hints' ]] && title=$QUTE_SELECTED_TEXT || title=$QUTE_TITLE
# try to add the task and grab the output
msg="$(task add $title $@ 2>&1)"
if [[ $? == 0 ]]; then
if msg="$(task add "$title" "$*" 2>&1)"; then
# annotate the new task with the url, send the output back to the browser
task +LATEST annotate "$QUTE_URL"
echo "message-info '$msg'" >> $QUTE_FIFO
echo "message-info '$msg'" >> "$QUTE_FIFO"
else
echo "message-error '$msg'" >> $QUTE_FIFO
echo "message-error '$msg'" >> "$QUTE_FIFO"
fi

View File

@ -50,7 +50,7 @@ msg() {
MPV_COMMAND=${MPV_COMMAND:-mpv}
# Warning: spaces in single flags are not supported
MPV_FLAGS=${MPV_FLAGS:- --force-window --no-terminal --keep-open=yes --ytdl --ytdl-raw-options=yes-playlist=}
video_command=( "$MPV_COMMAND" $MPV_FLAGS )
IFS=" " read -r -a video_command <<< "$MPV_COMMAND $MPV_FLAGS"
js() {
cat <<EOF

View File

@ -51,11 +51,12 @@ qt_log_ignore =
^Error when parsing the netrc file
^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=
^QPainter::end: Painter ended with \d+ saved states
^QSslSocket: cannot resolve *
^QSslSocket: cannot resolve .*
^QSslSocket: cannot call unresolved function .*
^Incompatible version of OpenSSL
^QQuickWidget::invalidateRenderControl could not make context current
^libpng warning: iCCP: known incorrect sRGB profile
^inotify_add_watch(".*") failed: "No space left on device"
^inotify_add_watch\(".*"\) failed: "No space left on device"
^QSettings::value: Empty key passed
^Icon theme ".*" not found
xfail_strict = true

View File

@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2017 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (1, 0, 3)
__version_info__ = (1, 0, 4)
__version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."

View File

@ -64,7 +64,7 @@ from qutebrowser.completion.models import miscmodels
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import config, websettings, configfiles, configinit
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads)
downloads, greasemonkey)
from qutebrowser.browser.network import proxy
from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.browser.webkit.network import networkmanager
@ -491,6 +491,9 @@ def _init_modules(args, crash_handler):
diskcache = cache.DiskCache(standarddir.cache(), parent=qApp)
objreg.register('cache', diskcache)
log.init.debug("Initializing Greasemonkey...")
greasemonkey.init()
log.init.debug("Misc initialization...")
macros.init()
# Init backend-specific stuff
@ -561,8 +564,8 @@ class Quitter:
cwd = os.path.abspath(os.path.dirname(sys.executable))
else:
args = [sys.executable, '-m', 'qutebrowser']
cwd = os.path.join(os.path.abspath(os.path.dirname(
qutebrowser.__file__)), '..')
cwd = os.path.join(
os.path.abspath(os.path.dirname(qutebrowser.__file__)), '..')
if not os.path.isdir(cwd):
# Probably running from a python egg. Let's fallback to
# cwd=None and see if that works out.

View File

@ -22,6 +22,7 @@
import enum
import itertools
import sip
import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt
from PyQt5.QtGui import QIcon
@ -486,6 +487,7 @@ class AbstractHistory:
raise NotImplementedError
def back(self, count=1):
"""Go back in the tab's history."""
idx = self.current_idx() - count
if idx >= 0:
self._go_to_item(self._item_at(idx))
@ -494,6 +496,7 @@ class AbstractHistory:
raise WebTabError("At beginning of history.")
def forward(self, count=1):
"""Go forward in the tab's history."""
idx = self.current_idx() + count
if idx < len(self):
self._go_to_item(self._item_at(idx))
@ -703,8 +706,8 @@ class AbstractTab(QWidget):
# This only gives us some mild protection against re-using events, but
# it's certainly better than a segfault.
if getattr(evt, 'posted', False):
raise AssertionError("Can't re-use an event which was already "
"posted!")
raise utils.Unreachable("Can't re-use an event which was already "
"posted!")
recipient = self.event_target()
evt.posted = True
QApplication.postEvent(recipient, evt)
@ -864,3 +867,6 @@ class AbstractTab(QWidget):
except (AttributeError, RuntimeError) as exc:
url = '<{}>'.format(exc.__class__.__name__)
return utils.get_repr(self, tab_id=self.tab_id, url=url)
def is_deleted(self):
return sip.isdeleted(self._widget)

View File

@ -39,7 +39,7 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem, downloads)
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, debug)
objreg, utils, debug, standarddir)
from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess
from qutebrowser.completion.models import urlmodel, miscmodels
@ -520,7 +520,7 @@ class CommandDispatcher:
return newtab
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', completion=miscmodels.buffer)
@cmdutils.argument('index', completion=miscmodels.other_buffer)
def tab_take(self, index):
"""Take a tab from another window.
@ -675,7 +675,7 @@ class CommandDispatcher:
self._open(new_url, tab, bg, window, related=True)
else: # pragma: no cover
raise ValueError("Got called with invalid value {} for "
"`where'.".format(where))
"`where'.".format(where))
except navigate.Error as e:
raise cmdexc.CommandError(e)
@ -1194,7 +1194,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_replace_variables=True)
def spawn(self, cmdline, userscript=False, verbose=False, detach=False):
def spawn(self, cmdline, userscript=False, verbose=False,
output=False, detach=False):
"""Spawn a command in a shell.
Args:
@ -1205,6 +1206,7 @@ class CommandDispatcher:
(or `$XDG_DATA_DIR`)
- `/usr/share/qutebrowser/userscripts`
verbose: Show notifications when the command started/exited.
output: Whether the output should be shown in a new tab.
detach: Whether the command should be detached from qutebrowser.
cmdline: The commandline to execute.
"""
@ -1241,6 +1243,11 @@ class CommandDispatcher:
else:
proc.start(cmd, args)
if output:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
tabbed_browser.openurl(QUrl('qute://spawn-output'), newtab=True)
@cmdutils.register(instance='command-dispatcher', scope='window')
def home(self):
"""Open main startpage in current tab."""
@ -1454,27 +1461,14 @@ class CommandDispatcher:
raise cmdexc.CommandError(e)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('dest_old', hide=True)
def download(self, url=None, dest_old=None, *, mhtml_=False, dest=None):
def download(self, url=None, *, mhtml_=False, dest=None):
"""Download a given URL, or current page if no URL given.
The form `:download [url] [dest]` is deprecated, use `:download --dest
[dest] [url]` instead.
Args:
url: The URL to download. If not given, download the current page.
dest_old: (deprecated) Same as dest.
dest: The file path to write the download to, or None to ask.
mhtml_: Download the current page and all assets as mhtml file.
"""
if dest_old is not None:
message.warning(":download [url] [dest] is deprecated - use "
":download --dest [dest] [url]")
if dest is not None:
raise cmdexc.CommandError("Can't give two destinations for the"
" download.")
dest = dest_old
# FIXME:qtwebengine do this with the QtWebEngine download manager?
download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window=self._win_id)
@ -1564,6 +1558,7 @@ class CommandDispatcher:
dest = os.path.expanduser(dest)
def callback(data):
"""Write the data to disk."""
try:
with open(dest, 'w', encoding='utf-8') as f:
f.write(data)
@ -1680,6 +1675,8 @@ class CommandDispatcher:
"""
try:
elem.set_value(text)
except webelem.OrphanedError as e:
message.error('Edited element vanished')
except webelem.Error as e:
raise cmdexc.CommandError(str(e))
@ -1776,7 +1773,8 @@ class CommandDispatcher:
elif going_up and tab.scroller.pos_px().y() > old_scroll_pos.y():
message.info("Search hit TOP, continuing at BOTTOM")
else:
message.warning("Text '{}' not found on page!".format(text))
message.warning("Text '{}' not found on page!".format(text),
replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
@ -1796,7 +1794,7 @@ class CommandDispatcher:
return
options = {
'ignore_case': config.val.ignore_case,
'ignore_case': config.val.search.ignore_case,
'reverse': reverse,
}
@ -2065,6 +2063,9 @@ class CommandDispatcher:
Args:
js_code: The string/file to evaluate.
file: Interpret js-code as a path to a file.
If the path is relative, the file is searched in a js/ subdir
in qutebrowser's data dir, e.g.
`~/.local/share/qutebrowser/js`.
quiet: Don't show resulting JS object.
world: Ignored on QtWebKit. On QtWebEngine, a world ID or name to
run the snippet in.
@ -2076,6 +2077,7 @@ class CommandDispatcher:
jseval_cb = None
else:
def jseval_cb(out):
"""Show the data returned from JS."""
if out is None:
# Getting the actual error (if any) seems to be difficult.
# The error does end up in
@ -2094,6 +2096,9 @@ class CommandDispatcher:
if file:
path = os.path.expanduser(js_code)
if not os.path.isabs(path):
path = os.path.join(standarddir.data(), 'js', path)
try:
with open(path, 'r', encoding='utf-8') as f:
js_code = f.read()

View File

@ -103,6 +103,8 @@ def immediate_download_path(prompt_download_directory=None):
if not prompt_download_directory:
return download_dir()
return None
def _path_suggestion(filename):
"""Get the suggested file path.
@ -180,7 +182,7 @@ def transform_path(path):
path = utils.expand_windows_drive(path)
# Drive dependent working directories are not supported, e.g.
# E:filename is invalid
if re.match(r'[A-Z]:[^\\]', path, re.IGNORECASE):
if re.search(r'^[A-Z]:[^\\]', path, re.IGNORECASE):
return None
# Paths like COM1, ...
# See https://github.com/qutebrowser/qutebrowser/issues/82
@ -990,7 +992,7 @@ class DownloadModel(QAbstractListModel):
if not count:
count = len(self)
raise cmdexc.CommandError("Download {} is already done!"
.format(count))
.format(count))
download.cancel()
@cmdutils.register(instance='download-model', scope='window')

View File

@ -0,0 +1,224 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Load, parse and make available Greasemonkey scripts."""
import re
import os
import json
import fnmatch
import functools
import glob
import attr
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.utils import log, standarddir, jinja, objreg
from qutebrowser.commands import cmdutils
def _scripts_dir():
"""Get the directory of the scripts."""
return os.path.join(standarddir.data(), 'greasemonkey')
class GreasemonkeyScript:
"""Container class for userscripts, parses metadata blocks."""
def __init__(self, properties, code):
self._code = code
self.includes = []
self.excludes = []
self.description = None
self.name = None
self.namespace = None
self.run_at = None
self.script_meta = None
self.runs_on_sub_frames = True
for name, value in properties:
if name == 'name':
self.name = value
elif name == 'namespace':
self.namespace = value
elif name == 'description':
self.description = value
elif name in ['include', 'match']:
self.includes.append(value)
elif name in ['exclude', 'exclude_match']:
self.excludes.append(value)
elif name == 'run-at':
self.run_at = value
elif name == 'noframes':
self.runs_on_sub_frames = False
HEADER_REGEX = r'// ==UserScript==|\n+// ==/UserScript==\n'
PROPS_REGEX = r'// @(?P<prop>[^\s]+)\s*(?P<val>.*)'
@classmethod
def parse(cls, source):
"""GreasemonkeyScript factory.
Takes a userscript source and returns a GreasemonkeyScript.
Parses the Greasemonkey metadata block, if present, to fill out
attributes.
"""
matches = re.split(cls.HEADER_REGEX, source, maxsplit=2)
try:
_head, props, _code = matches
except ValueError:
props = ""
script = cls(re.findall(cls.PROPS_REGEX, props), source)
script.script_meta = props
if not props:
script.includes = ['*']
return script
def code(self):
"""Return the processed JavaScript code of this script.
Adorns the source code with GM_* methods for Greasemonkey
compatibility and wraps it in an IFFE to hide it within a
lexical scope. Note that this means line numbers in your
browser's debugger/inspector will not match up to the line
numbers in the source script directly.
"""
return jinja.js_environment.get_template(
'greasemonkey_wrapper.js').render(
scriptName="/".join([self.namespace or '', self.name]),
scriptInfo=self._meta_json(),
scriptMeta=self.script_meta,
scriptSource=self._code)
def _meta_json(self):
return json.dumps({
'name': self.name,
'description': self.description,
'matches': self.includes,
'includes': self.includes,
'excludes': self.excludes,
'run-at': self.run_at,
})
@attr.s
class MatchingScripts(object):
"""All userscripts registered to run on a particular url."""
url = attr.ib()
start = attr.ib(default=attr.Factory(list))
end = attr.ib(default=attr.Factory(list))
idle = attr.ib(default=attr.Factory(list))
class GreasemonkeyManager(QObject):
"""Manager of userscripts and a Greasemonkey compatible environment.
Signals:
scripts_reloaded: Emitted when scripts are reloaded from disk.
Any cached or already-injected scripts should be
considered obselete.
"""
scripts_reloaded = pyqtSignal()
# https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes
# Limit the schemes scripts can run on due to unreasonable levels of
# exploitability
greaseable_schemes = ['http', 'https', 'ftp', 'file']
def __init__(self, parent=None):
super().__init__(parent)
self.load_scripts()
@cmdutils.register(name='greasemonkey-reload',
instance='greasemonkey')
def load_scripts(self):
"""Re-read Greasemonkey scripts from disk.
The scripts are read from a 'greasemonkey' subdirectory in
qutebrowser's data directory (see `:version`).
"""
self._run_start = []
self._run_end = []
self._run_idle = []
scripts_dir = os.path.abspath(_scripts_dir())
log.greasemonkey.debug("Reading scripts from: {}".format(scripts_dir))
for script_filename in glob.glob(os.path.join(scripts_dir, '*.js')):
if not os.path.isfile(script_filename):
continue
script_path = os.path.join(scripts_dir, script_filename)
with open(script_path, encoding='utf-8') as script_file:
script = GreasemonkeyScript.parse(script_file.read())
if not script.name:
script.name = script_filename
if script.run_at == 'document-start':
self._run_start.append(script)
elif script.run_at == 'document-end':
self._run_end.append(script)
elif script.run_at == 'document-idle':
self._run_idle.append(script)
else:
log.greasemonkey.warning("Script {} has invalid run-at "
"defined, defaulting to "
"document-end"
.format(script_path))
# Default as per
# https://wiki.greasespot.net/Metadata_Block#.40run-at
self._run_end.append(script)
log.greasemonkey.debug("Loaded script: {}".format(script.name))
self.scripts_reloaded.emit()
def scripts_for(self, url):
"""Fetch scripts that are registered to run for url.
returns a tuple of lists of scripts meant to run at (document-start,
document-end, document-idle)
"""
if url.scheme() not in self.greaseable_schemes:
return MatchingScripts(url, [], [], [])
match = functools.partial(fnmatch.fnmatch,
url.toString(QUrl.FullyEncoded))
tester = (lambda script:
any(match(pat) for pat in script.includes) and
not any(match(pat) for pat in script.excludes))
return MatchingScripts(
url,
[script for script in self._run_start if tester(script)],
[script for script in self._run_end if tester(script)],
[script for script in self._run_idle if tester(script)]
)
def all_scripts(self):
"""Return all scripts found in the configured script directory."""
return self._run_start + self._run_end + self._run_idle
def init():
"""Initialize Greasemonkey support."""
gm_manager = GreasemonkeyManager()
objreg.register('greasemonkey', gm_manager)
try:
os.mkdir(_scripts_dir())
except FileExistsError:
pass

View File

@ -390,10 +390,8 @@ class HintManager(QObject):
def _cleanup(self):
"""Clean up after hinting."""
# pylint: disable=not-an-iterable
for label in self._context.all_labels:
label.cleanup()
# pylint: enable=not-an-iterable
text = self._get_text()
message_bridge = objreg.get('message-bridge', scope='window',
@ -446,8 +444,17 @@ class HintManager(QObject):
# Short hints are the number of hints we can possibly show which are
# (needed - 1) digits in length.
if needed > min_chars:
short_count = math.floor((len(chars) ** needed - len(elems)) /
total_space = len(chars) ** needed
# Calculate short_count naively, by finding the avaiable space and
# dividing by the number of spots we would loose by adding a
# short element
short_count = math.floor((total_space - len(elems)) /
len(chars))
# Check if we double counted above to warrant another short_count
# https://github.com/qutebrowser/qutebrowser/issues/3242
if total_space - (short_count * len(chars) +
(len(elems) - short_count)) >= len(chars) - 1:
short_count += 1
else:
short_count = 0
@ -612,8 +619,9 @@ class HintManager(QObject):
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
star_args_optional=True, maxsplit=2)
@cmdutils.argument('win_id', win_id=True)
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
*args, win_id, mode=None, add_history=False):
def start(self, # pylint: disable=keyword-arg-before-vararg
group=webelem.Group.all, target=Target.normal,
*args, win_id, mode=None, add_history=False, rapid=False):
"""Start hinting.
Args:
@ -800,7 +808,6 @@ class HintManager(QObject):
log.hints.debug("Filtering hints on {!r}".format(filterstr))
visible = []
# pylint: disable=not-an-iterable
for label in self._context.all_labels:
try:
if self._filter_matches(filterstr, str(label.elem)):
@ -812,7 +819,6 @@ class HintManager(QObject):
label.hide()
except webelem.Error:
pass
# pylint: enable=not-an-iterable
if not visible:
# Whoops, filtered all hints

View File

@ -21,6 +21,7 @@
import os
import time
import contextlib
from PyQt5.QtCore import pyqtSlot, QUrl, QTimer
@ -87,6 +88,16 @@ class WebHistory(sql.SqlTable):
def __contains__(self, url):
return self._contains_query.run(val=url).value()
@contextlib.contextmanager
def _handle_sql_errors(self):
try:
yield
except sql.SqlError as e:
if e.environmental:
message.error("Failed to write history: {}".format(e.text()))
else:
raise
def _rebuild_completion(self):
data = {'url': [], 'title': [], 'last_atime': []}
# select the latest entry for each url
@ -138,12 +149,13 @@ class WebHistory(sql.SqlTable):
if force:
self._do_clear()
else:
message.confirm_async(self._do_clear, title="Clear all browsing "
"history?")
message.confirm_async(yes_action=self._do_clear,
title="Clear all browsing history?")
def _do_clear(self):
self.delete_all()
self.completion.delete_all()
with self._handle_sql_errors():
self.delete_all()
self.completion.delete_all()
def delete_url(self, url):
"""Remove all history entries with the given url.
@ -191,7 +203,7 @@ class WebHistory(sql.SqlTable):
atime = int(atime) if (atime is not None) else int(time.time())
try:
with self._handle_sql_errors():
self.insert({'url': self._format_url(url),
'title': title,
'atime': atime,
@ -202,11 +214,6 @@ class WebHistory(sql.SqlTable):
'title': title,
'last_atime': atime
}, replace=True)
except sql.SqlError as e:
if e.environmental:
message.error("Failed to write history: {}".format(e.text()))
else:
raise
def _parse_entry(self, line):
"""Parse a history line like '12345 http://example.com title'."""
@ -261,6 +268,7 @@ class WebHistory(sql.SqlTable):
return
def action():
"""Actually run the import."""
with debug.log_time(log.init, 'Import old history file to sqlite'):
try:
self._read(path)
@ -333,7 +341,7 @@ class WebHistory(sql.SqlTable):
f.write('\n'.join(lines))
message.info("Dumped history to {}".format(dest))
except OSError as e:
raise cmdexc.CommandError('Could not write history: {}', e)
raise cmdexc.CommandError('Could not write history: {}'.format(e))
def init(parent=None):

View File

@ -94,6 +94,7 @@ class AbstractWebInspector(QWidget):
raise NotImplementedError
def toggle(self, page):
"""Show/hide the inspector."""
if self._widget.isVisible():
self.hide()
else:

View File

@ -19,15 +19,13 @@
"""Mouse handling for a browser tab."""
from PyQt5.QtCore import QObject, QEvent, Qt, QTimer
from qutebrowser.config import config
from qutebrowser.utils import message, log, usertypes
from qutebrowser.keyinput import modeman
from PyQt5.QtCore import QObject, QEvent, Qt, QTimer
class ChildEventFilter(QObject):
"""An event filter re-adding MouseEventFilter on ChildEvent.

View File

@ -59,6 +59,7 @@ def _js_slot(*args):
def _decorator(method):
@functools.wraps(method)
def new_method(self, *args, **kwargs):
"""Call the underlying function."""
try:
return method(self, *args, **kwargs)
except:

View File

@ -82,7 +82,7 @@ def fix_urls(asset):
('viewer.css', 'qute://pdfjs/web/viewer.css'),
('compatibility.js', 'qute://pdfjs/web/compatibility.js'),
('locale/locale.properties',
'qute://pdfjs/web/locale/locale.properties'),
'qute://pdfjs/web/locale/locale.properties'),
('l10n.js', 'qute://pdfjs/web/l10n.js'),
('../build/pdf.js', 'qute://pdfjs/build/pdf.js'),
('debugger.js', 'qute://pdfjs/web/debugger.js'),

View File

@ -215,7 +215,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
abort_on=[self.cancelled, self.error])
def _set_fileobj(self, fileobj, *, autoclose=True):
""""Set the file object to write the download to.
"""Set the file object to write the download to.
Args:
fileobj: A file-like object.
@ -303,8 +303,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
"""Handle QNetworkReply errors."""
if code == QNetworkReply.OperationCanceledError:
return
else:
self._die(self._reply.errorString())
self._die(self._reply.errorString())
@pyqtSlot()
def _on_read_timer_timeout(self):
@ -399,7 +398,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
"""
if not url.isValid():
urlutils.invalid_url_error(url, "start download")
return
return None
req = QNetworkRequest(url)
if user_agent is not None:
req.setHeader(QNetworkRequest.UserAgentHeader, user_agent)

View File

@ -29,6 +29,7 @@ import os
import time
import textwrap
import mimetypes
import urllib
import pkg_resources
from PyQt5.QtCore import QUrlQuery, QUrl
@ -41,6 +42,7 @@ from qutebrowser.misc import objects
pyeval_output = ":pyeval was never called"
spawn_output = ":spawn was never called"
_HANDLERS = {}
@ -91,7 +93,7 @@ class Redirect(Exception):
self.url = url
class add_handler: # pylint: disable=invalid-name
class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to register a qute://* URL handler.
@ -111,6 +113,7 @@ class add_handler: # pylint: disable=invalid-name
return function
def wrapper(self, *args, **kwargs):
"""Call the underlying function."""
if self._backend is not None and objects.backend != self._backend:
return self.wrong_backend_handler(*args, **kwargs)
else:
@ -136,7 +139,7 @@ def data_for_url(url):
A (mimetype, data) tuple.
"""
norm_url = url.adjusted(QUrl.NormalizePathSegments |
QUrl.StripTrailingSlash)
QUrl.StripTrailingSlash)
if norm_url != url:
raise Redirect(norm_url)
@ -267,6 +270,13 @@ def qute_pyeval(_url):
return 'text/html', html
@add_handler('spawn-output')
def qute_spawn_output(_url):
"""Handler for qute://spawn-output."""
html = jinja.render('pre.html', title='spawn output', content=spawn_output)
return 'text/html', html
@add_handler('version')
@add_handler('verizon')
def qute_version(_url):
@ -425,6 +435,18 @@ def qute_settings(url):
return 'text/html', html
@add_handler('back')
def qute_back(url):
"""Handler for qute://back.
Simple page to free ram / lazy load a site, goes back on focusing the tab.
"""
html = jinja.render(
'back.html',
title='Suspended: ' + urllib.parse.unquote(url.fragment()))
return 'text/html', html
@add_handler('configdiff')
def qute_configdiff(url):
"""Handler for qute://configdiff."""
@ -433,7 +455,7 @@ def qute_configdiff(url):
return 'text/html', configdiff.get_diff()
except OSError as e:
error = (b'Failed to read old config: ' +
str(e.strerror).encode('utf-8'))
str(e.strerror).encode('utf-8'))
return 'text/plain', error
else:
data = config.instance.dump_userconfig().encode('utf-8')

View File

@ -22,7 +22,7 @@
import html
from qutebrowser.config import config
from qutebrowser.utils import usertypes, message, log, objreg, jinja
from qutebrowser.utils import usertypes, message, log, objreg, jinja, utils
from qutebrowser.mainwindow import mainwindow
@ -182,7 +182,7 @@ def ignore_certificate_errors(url, errors, abort_on):
return False
else:
raise ValueError("Invalid ssl_strict value {!r}".format(ssl_strict))
raise AssertionError("Not reached")
raise utils.Unreachable
def feature_permission(url, option, msg, yes_action, no_action, abort_on):

View File

@ -26,8 +26,8 @@ to a file on shutdown, so it makes sense to keep them as strings here.
"""
import os
import html
import os.path
import html
import functools
import collections

View File

@ -60,6 +60,13 @@ class Error(Exception):
pass
class OrphanedError(Error):
"""Raised when a webelement's parent has vanished."""
pass
class AbstractWebElement(collections.abc.MutableMapping):
"""A wrapper around QtWebKit/QtWebEngine web element.
@ -221,7 +228,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
}
relevant_classes = classes[self.tag_name()]
for klass in self.classes():
if any([klass.strip().startswith(e) for e in relevant_classes]):
if any(klass.strip().startswith(e) for e in relevant_classes):
return True
return False

View File

@ -30,7 +30,7 @@ from qutebrowser.utils import log
def version(filename):
"""Extract the version number from the dictionary file name."""
version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
match = version_re.match(filename)
match = version_re.fullmatch(filename)
if match is None:
raise ValueError('the given dictionary file name is malformed: {}'
.format(filename))

View File

@ -100,6 +100,8 @@ class WebEngineElement(webelem.AbstractWebElement):
def _js_call(self, name, *args, callback=None):
"""Wrapper to run stuff from webelem.js."""
if self._tab.is_deleted():
raise webelem.OrphanedError("Tab containing element vanished")
js_code = javascript.assemble('webelem', name, self._id, *args)
self._tab.run_js_async(js_code, callback=callback)
@ -209,11 +211,11 @@ class WebEngineElement(webelem.AbstractWebElement):
def _click_js(self, _click_target):
# FIXME:qtwebengine Have a proper API for this
# pylint: disable=protected-access
settings = self._tab._widget.settings()
view = self._tab._widget
# pylint: enable=protected-access
attribute = QWebEngineSettings.JavascriptCanOpenWindows
could_open_windows = settings.testAttribute(attribute)
settings.setAttribute(attribute, True)
could_open_windows = view.settings().testAttribute(attribute)
view.settings().setAttribute(attribute, True)
# Get QtWebEngine do apply the settings
# (it does so with a 0ms QTimer...)
@ -224,6 +226,12 @@ class WebEngineElement(webelem.AbstractWebElement):
QEventLoop.ExcludeUserInputEvents)
def reset_setting(_arg):
settings.setAttribute(attribute, could_open_windows)
"""Set the JavascriptCanOpenWindows setting to its old value."""
try:
view.settings().setAttribute(attribute, could_open_windows)
except RuntimeError:
# Happens if this callback gets called during QWebEnginePage
# destruction, i.e. if the tab was closed in the meantime.
pass
self._js_call('click', callback=reset_setting)

View File

@ -29,6 +29,7 @@ Module attributes:
import os
import sip
from PyQt5.QtGui import QFont
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
QWebEngineScript)
@ -37,7 +38,7 @@ from qutebrowser.browser import shared
from qutebrowser.browser.webengine import spell
from qutebrowser.config import config, websettings
from qutebrowser.utils import (utils, standarddir, javascript, qtutils,
message, log)
message, log, objreg)
# The default QWebEngineProfile
default_profile = None
@ -93,9 +94,10 @@ class DefaultProfileSetter(websettings.Base):
"""A setting set on the QWebEngineProfile."""
def __init__(self, setter, default=websettings.UNSET):
def __init__(self, setter, converter=None, default=websettings.UNSET):
super().__init__(default)
self._setter = setter
self._converter = converter
def __repr__(self):
return utils.get_repr(self, setter=self._setter, constructor=True)
@ -104,7 +106,11 @@ class DefaultProfileSetter(websettings.Base):
if settings is not None:
raise ValueError("'settings' may not be set with "
"DefaultProfileSetters!")
setter = getattr(default_profile, self._setter)
if self._converter is not None:
value = self._converter(value)
setter(value)
@ -153,33 +159,44 @@ class DictionaryLanguageSetter(DefaultProfileSetter):
def _init_stylesheet(profile):
"""Initialize custom stylesheets.
Mostly inspired by QupZilla:
Partially inspired by QupZilla:
https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101
https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/tools/scripts.cpp#L119-L132
"""
old_script = profile.scripts().findScript('_qute_stylesheet')
if not old_script.isNull():
profile.scripts().remove(old_script)
css = shared.get_user_stylesheet()
source = """
(function() {{
var css = document.createElement('style');
css.setAttribute('type', 'text/css');
css.appendChild(document.createTextNode('{}'));
document.getElementsByTagName('head')[0].appendChild(css);
}})()
""".format(javascript.string_escape(css))
source = '\n'.join([
'"use strict";',
'window._qutebrowser = window._qutebrowser || {};',
utils.read_file('javascript/stylesheet.js'),
javascript.assemble('stylesheet', 'set_css', css),
])
script = QWebEngineScript()
script.setName('_qute_stylesheet')
script.setInjectionPoint(QWebEngineScript.DocumentReady)
script.setInjectionPoint(QWebEngineScript.DocumentCreation)
script.setWorldId(QWebEngineScript.ApplicationWorld)
script.setRunsOnSubFrames(True)
script.setSourceCode(source)
profile.scripts().insert(script)
def _update_stylesheet():
"""Update the custom stylesheet in existing tabs."""
css = shared.get_user_stylesheet()
code = javascript.assemble('stylesheet', 'set_css', css)
for win_id, window in objreg.window_registry.items():
# We could be in the middle of destroying a window here
if sip.isdeleted(window):
continue
tab_registry = objreg.get('tab-registry', scope='window',
window=win_id)
for tab in tab_registry.values():
tab.run_js_async(code)
def _set_http_headers(profile):
"""Set the user agent and accept-language for the given profile.
@ -199,6 +216,7 @@ def _update_settings(option):
if option in ['scrolling.bar', 'content.user_stylesheets']:
_init_stylesheet(default_profile)
_init_stylesheet(private_profile)
_update_stylesheet()
elif option in ['content.headers.user_agent',
'content.headers.accept_language']:
_set_http_headers(default_profile)
@ -226,6 +244,43 @@ def _init_profiles():
private_profile.setSpellCheckEnabled(True)
def inject_userscripts():
"""Register user JavaScript files with the global profiles."""
# The Greasemonkey metadata block support in QtWebEngine only starts at
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in response
# to urlChanged.
if not qtutils.version_check('5.8'):
return
# Since we are inserting scripts into profile.scripts they won't
# just get replaced by new gm scripts like if we were injecting them
# ourselves so we need to remove all gm scripts, while not removing
# any other stuff that might have been added. Like the one for
# stylesheets.
greasemonkey = objreg.get('greasemonkey')
for profile in [default_profile, private_profile]:
scripts = profile.scripts()
for script in scripts.toList():
if script.name().startswith("GM-"):
log.greasemonkey.debug('Removing script: {}'
.format(script.name()))
removed = scripts.remove(script)
assert removed, script.name()
# Then add the new scripts.
for script in greasemonkey.all_scripts():
# @run-at (and @include/@exclude/@match) is parsed by
# QWebEngineScript.
new_script = QWebEngineScript()
new_script.setWorldId(QWebEngineScript.MainWorld)
new_script.setSourceCode(script.code())
new_script.setName("GM-{}".format(script.name))
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
log.greasemonkey.debug('adding script: {}'
.format(new_script.name()))
scripts.insert(new_script)
def init(args):
"""Initialize the global QWebSettings."""
if args.enable_webengine_inspector:
@ -283,7 +338,9 @@ MAPPINGS = {
Attribute(QWebEngineSettings.LocalStorageEnabled),
'content.cache.size':
# 0: automatically managed by QtWebEngine
DefaultProfileSetter('setHttpCacheMaximumSize', default=0),
DefaultProfileSetter('setHttpCacheMaximumSize', default=0,
converter=lambda val:
qtutils.check_overflow(val, 'int', fatal=False)),
'content.xss_auditing':
Attribute(QWebEngineSettings.XSSAuditingEnabled),
'content.default_encoding':

View File

@ -24,7 +24,8 @@ import functools
import html as html_utils
import sip
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QPointF, QUrl, QTimer
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF,
QUrl, QTimer)
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication
@ -69,6 +70,10 @@ def init():
download_manager.install(webenginesettings.private_profile)
objreg.register('webengine-download-manager', download_manager)
greasemonkey = objreg.get('greasemonkey')
greasemonkey.scripts_reloaded.connect(webenginesettings.inject_userscripts)
webenginesettings.inject_userscripts()
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
_JS_WORLD_MAP = {
@ -121,18 +126,35 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
class WebEngineSearch(browsertab.AbstractSearch):
"""QtWebEngine implementations related to searching on the page."""
"""QtWebEngine implementations related to searching on the page.
Attributes:
_flags: The QWebEnginePage.FindFlags of the last search.
_pending_searches: How many searches have been started but not called
back yet.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._flags = QWebEnginePage.FindFlags(0)
self._pending_searches = 0
def _find(self, text, flags, callback, caller):
"""Call findText on the widget."""
self.search_displayed = True
self._pending_searches += 1
def wrapped_callback(found):
"""Wrap the callback to do debug logging."""
self._pending_searches -= 1
if self._pending_searches > 0:
# See https://github.com/qutebrowser/qutebrowser/issues/2442
# and https://github.com/qt/qtwebengine/blob/5.10/src/core/web_contents_adapter.cpp#L924-L934
log.webview.debug("Ignoring cancelled search callback with "
"{} pending searches".format(
self._pending_searches))
return
found_text = 'found' if found else "didn't find"
if flags:
flag_text = 'with flags {}'.format(debug.qflags_key(
@ -560,7 +582,15 @@ class WebEngineElements(browsertab.AbstractElements):
class WebEngineTab(browsertab.AbstractTab):
"""A QtWebEngine tab in the browser."""
"""A QtWebEngine tab in the browser.
Signals:
_load_finished_fake:
Used in place of unreliable loadFinished
"""
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223
_load_finished_fake = pyqtSignal(bool)
def __init__(self, *, win_id, mode_manager, private, parent=None):
super().__init__(win_id=win_id, mode_manager=mode_manager,
@ -586,7 +616,7 @@ class WebEngineTab(browsertab.AbstractTab):
def _init_js(self):
js_code = '\n'.join([
'"use strict";',
'window._qutebrowser = {};',
'window._qutebrowser = window._qutebrowser || {};',
utils.read_file('javascript/scroll.js'),
utils.read_file('javascript/webelem.js'),
utils.read_file('javascript/webengine_caret.js'),
@ -815,6 +845,24 @@ class WebEngineTab(browsertab.AbstractTab):
}
self.renderer_process_terminated.emit(status_map[status], exitcode)
@pyqtSlot(int)
def _on_load_progress_workaround(self, perc):
"""Use loadProgress(100) to emit loadFinished(True).
See https://bugreports.qt.io/browse/QTBUG-65223
"""
if perc == 100 and self.load_status() != usertypes.LoadStatus.error:
self._load_finished_fake.emit(True)
@pyqtSlot(bool)
def _on_load_finished_workaround(self, ok):
"""Use only loadFinished(False).
See https://bugreports.qt.io/browse/QTBUG-65223
"""
if not ok:
self._load_finished_fake.emit(False)
def _connect_signals(self):
view = self._widget
page = view.page()
@ -823,9 +871,6 @@ class WebEngineTab(browsertab.AbstractTab):
page.linkHovered.connect(self.link_hovered)
page.loadProgress.connect(self._on_load_progress)
page.loadStarted.connect(self._on_load_started)
page.loadFinished.connect(self._on_history_trigger)
page.loadFinished.connect(self._restore_zoom)
page.loadFinished.connect(self._on_load_finished)
page.certificate_error.connect(self._on_ssl_errors)
page.authenticationRequired.connect(self._on_authentication_required)
page.proxyAuthenticationRequired.connect(
@ -838,6 +883,19 @@ class WebEngineTab(browsertab.AbstractTab):
view.renderProcessTerminated.connect(
self._on_render_process_terminated)
view.iconChanged.connect(self.icon_changed)
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223
if qtutils.version_check('5.10', compiled=False):
page.loadProgress.connect(self._on_load_progress_workaround)
self._load_finished_fake.connect(self._on_history_trigger)
self._load_finished_fake.connect(self._restore_zoom)
self._load_finished_fake.connect(self._on_load_finished)
page.loadFinished.connect(self._on_load_finished_workaround)
else:
# for older Qt versions which break with the above
page.loadProgress.connect(self._on_load_progress)
page.loadFinished.connect(self._on_history_trigger)
page.loadFinished.connect(self._restore_zoom)
page.loadFinished.connect(self._on_load_finished)
def event_target(self):
return self._widget.focusProxy()

View File

@ -23,12 +23,14 @@ import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
from PyQt5.QtGui import QPalette
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage,
QWebEngineScript)
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import certificateerror, webenginesettings
from qutebrowser.config import config
from qutebrowser.utils import log, debug, usertypes, jinja, urlutils, message
from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message,
objreg, qtutils)
class WebEngineView(QWebEngineView):
@ -135,6 +137,7 @@ class WebEnginePage(QWebEnginePage):
self._theme_color = theme_color
self._set_bg_color()
config.instance.changed.connect(self._set_bg_color)
self.urlChanged.connect(self._inject_userjs)
@config.change_filter('colors.webpage.bg')
def _set_bg_color(self):
@ -300,3 +303,43 @@ class WebEnginePage(QWebEnginePage):
message.error(msg)
return False
return True
@pyqtSlot('QUrl')
def _inject_userjs(self, url):
"""Inject userscripts registered for `url` into the current page."""
if qtutils.version_check('5.8'):
# Handled in webenginetab with the builtin Greasemonkey
# support.
return
# Using QWebEnginePage.scripts() to hold the user scripts means
# we don't have to worry ourselves about where to inject the
# page but also means scripts hang around for the tab lifecycle.
# So clear them here.
scripts = self.scripts()
for script in scripts.toList():
if script.name().startswith("GM-"):
log.greasemonkey.debug("Removing script: {}"
.format(script.name()))
removed = scripts.remove(script)
assert removed, script.name()
def _add_script(script, injection_point):
new_script = QWebEngineScript()
new_script.setInjectionPoint(injection_point)
new_script.setWorldId(QWebEngineScript.MainWorld)
new_script.setSourceCode(script.code())
new_script.setName("GM-{}".format(script.name))
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
log.greasemonkey.debug("Adding script: {}"
.format(new_script.name()))
scripts.insert(new_script)
greasemonkey = objreg.get('greasemonkey')
matching_scripts = greasemonkey.scripts_for(url)
for script in matching_scripts.start:
_add_script(script, QWebEngineScript.DocumentCreation)
for script in matching_scripts.end:
_add_script(script, QWebEngineScript.DocumentReady)
for script in matching_scripts.idle:
_add_script(script, QWebEngineScript.Deferred)

View File

@ -22,11 +22,11 @@
import os.path
from PyQt5.QtNetwork import QNetworkRequest
from qutebrowser.utils import log
from qutebrowser.browser.webkit import rfc6266
from PyQt5.QtNetwork import QNetworkRequest
def parse_content_disposition(reply):
"""Parse a content_disposition header.
@ -57,9 +57,7 @@ def parse_content_disposition(reply):
is_inline = content_disposition.is_inline()
# Then try to get filename from url
if not filename:
path = reply.url().path()
if path is not None:
filename = path.rstrip('/')
filename = reply.url().path().rstrip('/')
# If that fails as well, use a fallback
if not filename:
filename = 'qutebrowser-download'

View File

@ -502,8 +502,8 @@ class _Downloader:
This is needed if a download finishes before attaching its
finished signal.
"""
items = set((url, item) for url, item in self.pending_downloads
if item.done)
items = {(url, item) for url, item in self.pending_downloads
if item.done}
log.downloads.debug("Zombie downloads: {}".format(items))
for url, item in items:
self._finished(url, item)

View File

@ -132,5 +132,6 @@ class FileSchemeHandler(schemehandler.SchemeHandler):
data = dirbrowser_html(path)
return networkreply.FixedDataNetworkReply(
request, data, 'text/html', self.parent())
return None
except UnicodeEncodeError:
return None

View File

@ -206,7 +206,7 @@ class NetworkManager(QNetworkAccessManager):
# No @pyqtSlot here, see
# https://github.com/qutebrowser/qutebrowser/issues/2213
def on_ssl_errors(self, reply, errors): # pragma: no mccabe
def on_ssl_errors(self, reply, errors): # noqa: C901 pragma: no mccabe
"""Decide if SSL errors should be ignored or not.
This slot is called on SSL/TLS errors by the self.sslErrors signal.

View File

@ -34,7 +34,7 @@ class FixedDataNetworkReply(QNetworkReply):
"""QNetworkReply subclass for fixed data."""
def __init__(self, request, fileData, mimeType, # flake8: disable=N803
def __init__(self, request, fileData, mimeType, # noqa: N803
parent=None):
"""Constructor.

View File

@ -270,6 +270,7 @@ class _ContentDisposition:
elif 'filename' in self.assocs:
# XXX Reject non-ascii (parsed via qdtext) here?
return self.assocs['filename']
return None
def is_inline(self):
"""Return if the file should be handled inline.

View File

@ -118,6 +118,8 @@ class WebKitElement(webelem.AbstractWebElement):
def set_value(self, value):
self._check_vanished()
if self._tab.is_deleted():
raise webelem.OrphanedError("Tab containing element vanished")
if self.is_content_editable():
log.webelem.debug("Filling {!r} via set_text.".format(self))
self._elem.setPlainText(value)

View File

@ -19,6 +19,7 @@
"""Wrapper over our (QtWebKit) WebView."""
import re
import functools
import xml.etree.ElementTree
@ -545,10 +546,15 @@ class WebKitElements(browsertab.AbstractElements):
def find_id(self, elem_id, callback):
def find_id_cb(elems):
"""Call the real callback with the found elements."""
if not elems:
callback(None)
else:
callback(elems[0])
# Escape non-alphanumeric characters in the selector
# https://www.w3.org/TR/CSS2/syndata.html#value-def-identifier
elem_id = re.sub(r'[^a-zA-Z0-9_-]', r'\\\g<0>', elem_id)
self.find_css('#' + elem_id, find_id_cb)
def find_focused(self, callback):

View File

@ -86,6 +86,21 @@ class BrowserPage(QWebPage):
self.on_save_frame_state_requested)
self.restoreFrameStateRequested.connect(
self.on_restore_frame_state_requested)
self.loadFinished.connect(
functools.partial(self._inject_userjs, self.mainFrame()))
self.frameCreated.connect(self._connect_userjs_signals)
@pyqtSlot('QWebFrame*')
def _connect_userjs_signals(self, frame):
"""Connect userjs related signals to `frame`.
Connect the signals used as triggers for injecting user
JavaScripts into the passed QWebFrame.
"""
log.greasemonkey.debug("Connecting to frame {} ({})"
.format(frame, frame.url().toDisplayString()))
frame.loadFinished.connect(
functools.partial(self._inject_userjs, frame))
def javaScriptPrompt(self, frame, js_msg, default):
"""Override javaScriptPrompt to use qutebrowser prompts."""
@ -283,6 +298,38 @@ class BrowserPage(QWebPage):
else:
self.error_occurred = False
def _inject_userjs(self, frame):
"""Inject user JavaScripts into the page.
Args:
frame: The QWebFrame to inject the user scripts into.
"""
url = frame.url()
if url.isEmpty():
url = frame.requestedUrl()
log.greasemonkey.debug("_inject_userjs called for {} ({})"
.format(frame, url.toDisplayString()))
greasemonkey = objreg.get('greasemonkey')
scripts = greasemonkey.scripts_for(url)
# QtWebKit has trouble providing us with signals representing
# page load progress at reasonable times, so we just load all
# scripts on the same event.
toload = scripts.start + scripts.end + scripts.idle
if url.isEmpty():
# This happens during normal usage like with view source but may
# also indicate a bug.
log.greasemonkey.debug("Not running scripts for frame with no "
"url: {}".format(frame))
assert not toload, toload
for script in toload:
if frame is self.mainFrame() or script.runs_on_sub_frames:
log.webview.debug('Running GM script: {}'.format(script.name))
frame.evaluateJavaScript(script.code())
@pyqtSlot('QWebFrame*', 'QWebPage::Feature')
def _on_feature_permission_requested(self, frame, feature):
"""Ask the user for approval for geolocation/notifications."""

View File

@ -61,7 +61,7 @@ def check_exclusive(flags, names):
argstr))
class register: # pylint: disable=invalid-name
class register: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to register a new command handler.
@ -114,7 +114,7 @@ class register: # pylint: disable=invalid-name
return func
class argument: # pylint: disable=invalid-name
class argument: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to customize an argument for @cmdutils.register.

View File

@ -190,6 +190,7 @@ class Command:
return True
elif arg_info.win_id:
return True
return False
def _inspect_func(self):
"""Inspect the function to get useful informations from it.
@ -393,7 +394,7 @@ class Command:
if isinstance(typ, tuple):
raise TypeError("{}: Legacy tuple type annotation!".format(
self.name))
elif type(typ) is type(typing.Union): # flake8: disable=E721
elif type(typ) is type(typing.Union): # noqa: E721
# this is... slightly evil, I know
# We also can't use isinstance here because typing.Union doesn't
# support that.

View File

@ -95,7 +95,6 @@ class CommandParser:
"""Parse qutebrowser commandline commands.
Attributes:
_partial_match: Whether to allow partial command matches.
"""
@ -127,7 +126,7 @@ class CommandParser:
new_cmd += ' '
return new_cmd
def _parse_all_gen(self, text, aliases=True, *args, **kwargs):
def _parse_all_gen(self, text, *args, aliases=True, **kwargs):
"""Split a command on ;; and parse all parts.
If the first command in the commandline is a non-split one, it only

View File

@ -35,6 +35,7 @@ class CompletionInfo:
config = attr.ib()
keyconf = attr.ib()
win_id = attr.ib()
class Completer(QObject):
@ -43,6 +44,7 @@ class Completer(QObject):
Attributes:
_cmd: The statusbar Command object this completer belongs to.
_win_id: The id of the window that owns this object.
_timer: The timer used to trigger the completion update.
_last_cursor_pos: The old cursor position so we avoid double completion
updates.
@ -50,9 +52,10 @@ class Completer(QObject):
_last_completion_func: The completion function used for the last text.
"""
def __init__(self, cmd, parent=None):
def __init__(self, *, cmd, win_id, parent=None):
super().__init__(parent)
self._cmd = cmd
self._win_id = win_id
self._timer = QTimer()
self._timer.setSingleShot(True)
self._timer.setInterval(0)
@ -131,9 +134,7 @@ class Completer(QObject):
return [], '', []
parser = runners.CommandParser()
result = parser.parse(text, fallback=True, keep=True)
# pylint: disable=not-an-iterable
parts = [x for x in result.cmdline if x]
# pylint: enable=not-an-iterable
pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars
log.completion.debug('partitioning {} around position {}'.format(parts,
@ -152,8 +153,7 @@ class Completer(QObject):
"partitioned: {} '{}' {}".format(prefix, center, postfix))
return prefix, center, postfix
# We should always return above
assert False, parts
raise utils.Unreachable("Not all parts consumed: {}".format(parts))
@pyqtSlot(str)
def on_selection_changed(self, text):
@ -206,7 +206,7 @@ class Completer(QObject):
log.completion.debug("Ignoring update because the length of "
"the text is less than completion.min_chars.")
elif (self._cmd.cursorPosition() == self._last_cursor_pos and
self._cmd.text() == self._last_text):
self._cmd.text() == self._last_text):
log.completion.debug("Ignoring update because there were no "
"changes.")
else:
@ -247,10 +247,11 @@ class Completer(QObject):
if func != self._last_completion_func:
self._last_completion_func = func
args = (x for x in before_cursor[1:] if not x.startswith('-'))
with debug.log_time(log.completion,
'Starting {} completion'.format(func.__name__)):
with debug.log_time(log.completion, 'Starting {} completion'
.format(func.__name__)):
info = CompletionInfo(config=config.instance,
keyconf=config.key_instance)
keyconf=config.key_instance,
win_id=self._win_id)
model = func(*args, info=info)
with debug.log_time(log.completion, 'Set completion model'):
completion.set_model(model)

View File

@ -138,10 +138,10 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._painter.translate(text_rect.left(), text_rect.top())
self._get_textdoc(index)
self._draw_textdoc(text_rect)
self._draw_textdoc(text_rect, index.column())
self._painter.restore()
def _draw_textdoc(self, rect):
def _draw_textdoc(self, rect, col):
"""Draw the QTextDocument of an item.
Args:
@ -156,7 +156,9 @@ class CompletionItemDelegate(QStyledItemDelegate):
elif not self._opt.state & QStyle.State_Enabled:
color = config.val.colors.completion.category.fg
else:
color = config.val.colors.completion.fg
colors = config.val.colors.completion.fg
# if multiple colors are set, use different colors per column
color = colors[col % len(colors)]
self._painter.setPen(color)
ctx = QAbstractTextDocumentLayout.PaintContext()

View File

@ -23,7 +23,7 @@ Defines a CompletionView which uses CompletionFiterModel and CompletionModel
subclasses to provide completions.
"""
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy, QStyleFactory
from PyQt5.QtWidgets import QTreeView, QSizePolicy, QStyleFactory
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config
@ -152,12 +152,12 @@ class CompletionView(QTreeView):
column_widths = self.model().column_widths
pixel_widths = [(width * perc // 100) for perc in column_widths]
if self.verticalScrollBar().isVisible():
delta = self.style().pixelMetric(QStyle.PM_ScrollBarExtent) + 5
if pixel_widths[-1] > delta:
pixel_widths[-1] -= delta
else:
pixel_widths[-2] -= delta
delta = self.verticalScrollBar().sizeHint().width()
if pixel_widths[-1] > delta:
pixel_widths[-1] -= delta
else:
pixel_widths[-2] -= delta
for i, w in enumerate(pixel_widths):
assert w >= 0, i
self.setColumnWidth(i, w)
@ -180,6 +180,7 @@ class CompletionView(QTreeView):
return self.model().last_item()
else:
return self.model().first_item()
while True:
idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)
# wrap around if we arrived at beginning/end
@ -193,6 +194,8 @@ class CompletionView(QTreeView):
# Item is a real item, not a category header -> success
return idx
raise utils.Unreachable
def _next_category_idx(self, upwards):
"""Get the index of the previous/next category.
@ -222,30 +225,46 @@ class CompletionView(QTreeView):
self.scrollTo(idx)
return idx.child(0, 0)
raise utils.Unreachable
@cmdutils.register(instance='completion',
modes=[usertypes.KeyMode.command], scope='window')
@cmdutils.argument('which', choices=['next', 'prev', 'next-category',
'prev-category'])
def completion_item_focus(self, which):
@cmdutils.argument('history', flag='H')
def completion_item_focus(self, which, history=False):
"""Shift the focus of the completion menu to another item.
Args:
which: 'next', 'prev', 'next-category', or 'prev-category'.
history: Navigate through command history if no text was typed.
"""
if history:
status = objreg.get('status-command', scope='window',
window=self._win_id)
if (status.text() == ':' or status.history.is_browsing() or
not self._active):
if which == 'next':
status.command_history_next()
return
elif which == 'prev':
status.command_history_prev()
return
else:
raise cmdexc.CommandError("Can't combine --history with "
"{}!".format(which))
if not self._active:
return
selmodel = self.selectionModel()
if which == 'next':
idx = self._next_idx(upwards=False)
elif which == 'prev':
idx = self._next_idx(upwards=True)
elif which == 'next-category':
idx = self._next_category_idx(upwards=False)
elif which == 'prev-category':
idx = self._next_category_idx(upwards=True)
else: # pragma: no cover
raise ValueError("Invalid 'which' value {!r}".format(which))
selmodel = self.selectionModel()
indices = {
'next': self._next_idx(upwards=False),
'prev': self._next_idx(upwards=True),
'next-category': self._next_category_idx(upwards=False),
'prev-category': self._next_category_idx(upwards=True),
}
idx = indices[which]
if not idx.isValid():
return

View File

@ -60,8 +60,6 @@ class CompletionModel(QAbstractItemModel):
def add_category(self, cat):
"""Add a completion category to the model."""
self._categories.append(cat)
cat.layoutAboutToBeChanged.connect(self.layoutAboutToBeChanged)
cat.layoutChanged.connect(self.layoutChanged)
def data(self, index, role=Qt.DisplayRole):
"""Return the item data for index.
@ -179,8 +177,13 @@ class CompletionModel(QAbstractItemModel):
pattern: The filter pattern to set.
"""
log.completion.debug("Setting completion pattern '{}'".format(pattern))
# WORKAROUND:
# layoutChanged is broken in PyQt 5.7.1, so we must use metaObject
# https://www.riverbankcomputing.com/pipermail/pyqt/2017-January/038483.html
self.metaObject().invokeMethod(self, "layoutAboutToBeChanged")
for cat in self._categories:
cat.set_pattern(pattern)
self.metaObject().invokeMethod(self, "layoutChanged")
def first_item(self):
"""Return the index of the first child (non-category) in the model."""

View File

@ -60,7 +60,8 @@ def value(optname, *_values, info):
opt = info.config.get_opt(optname)
default = opt.typ.to_str(opt.default)
cur_cat = listcategory.ListCategory("Current/Default",
cur_cat = listcategory.ListCategory(
"Current/Default",
[(current, "Current value"), (default, "Default value")])
model.add_category(cur_cat)
@ -77,17 +78,26 @@ def bind(key, *, info):
key: the key being bound.
"""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmd_text = info.keyconf.get_command(key, 'normal')
data = []
cmd_text = info.keyconf.get_command(key, 'normal')
if cmd_text:
parser = runners.CommandParser()
try:
cmd = parser.parse(cmd_text).cmd
except cmdexc.NoSuchCommandError:
data = [(cmd_text, 'Invalid command!', key)]
data.append((cmd_text, '(Current) Invalid command!', key))
else:
data = [(cmd_text, cmd.desc, key)]
model.add_category(listcategory.ListCategory("Current", data))
data.append((cmd_text, '(Current) {}'.format(cmd.desc), key))
cmd_text = info.keyconf.get_command(key, 'normal', default=True)
if cmd_text:
parser = runners.CommandParser()
cmd = parser.parse(cmd_text).cmd
data.append((cmd_text, '(Default) {}'.format(cmd.desc), key))
if data:
model.add_category(listcategory.ListCategory("Current/Default", data))
cmdlist = util.get_cmd_completions(info, include_hidden=True,
include_aliases=True)

View File

@ -19,8 +19,6 @@
"""A completion category that queries the SQL History store."""
import re
from PyQt5.QtSql import QSqlQueryModel
from qutebrowser.misc import sql
@ -36,21 +34,7 @@ class HistoryCategory(QSqlQueryModel):
"""Create a new History completion category."""
super().__init__(parent=parent)
self.name = "History"
# replace ' in timestamp-format to avoid breaking the query
timestamp_format = config.val.completion.timestamp_format
timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')"
.format(timestamp_format.replace("'", "`")))
self._query = sql.Query(' '.join([
"SELECT url, title, {}".format(timefmt),
"FROM CompletionHistory",
# the incoming pattern will have literal % and _ escaped with '\'
# we need to tell sql to treat '\' as an escape character
"WHERE (url LIKE :pat escape '\\' or title LIKE :pat escape '\\')",
self._atime_expr(),
"ORDER BY last_atime DESC",
]), forward_only=False)
self._query = None
# advertise that this model filters by URL and title
self.columns_to_filter = [0, 1]
@ -86,11 +70,36 @@ class HistoryCategory(QSqlQueryModel):
# escape to treat a user input % or _ as a literal, not a wildcard
pattern = pattern.replace('%', '\\%')
pattern = pattern.replace('_', '\\_')
# treat spaces as wildcards to match any of the typed words
pattern = re.sub(r' +', '%', pattern)
pattern = '%{}%'.format(pattern)
words = ['%{}%'.format(w) for w in pattern.split(' ')]
# build a where clause to match all of the words in any order
# given the search term "a b", the WHERE clause would be:
# ((url || title) LIKE '%a%') AND ((url || title) LIKE '%b%')
where_clause = ' AND '.join(
"(url || title) LIKE :{} escape '\\'".format(i)
for i in range(len(words)))
# replace ' in timestamp-format to avoid breaking the query
timestamp_format = config.val.completion.timestamp_format
timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')"
.format(timestamp_format.replace("'", "`")))
if not self._query or len(words) != len(self._query.boundValues()):
# if the number of words changed, we need to generate a new query
# otherwise, we can reuse the prepared query for performance
self._query = sql.Query(' '.join([
"SELECT url, title, {}".format(timefmt),
"FROM CompletionHistory",
# the incoming pattern will have literal % and _ escaped
# we need to tell sql to treat '\' as an escape character
'WHERE ({})'.format(where_clause),
self._atime_expr(),
"ORDER BY last_atime DESC",
]), forward_only=False)
with debug.log_time('sql', 'Running completion query'):
self._query.run(pat=pattern)
self._query.run(**{
str(i): w for i, w in enumerate(words)})
self.setQuery(self._query)
def removeRows(self, row, _count, _parent=None):

View File

@ -94,10 +94,11 @@ def session(*, info=None): # pylint: disable=unused-argument
return model
def buffer(*, info=None): # pylint: disable=unused-argument
"""A model to complete on open tabs across all windows.
def _buffer(skip_win_id=None):
"""Helper to get the completion model for buffer/other_buffer.
Used for switching the buffer command.
Args:
skip_win_id: The id of the window to skip, or None to include all.
"""
def delete_buffer(data):
"""Close the selected tab."""
@ -109,6 +110,8 @@ def buffer(*, info=None): # pylint: disable=unused-argument
model = completionmodel.CompletionModel(column_widths=(6, 40, 54))
for win_id in objreg.window_registry:
if skip_win_id and win_id == skip_win_id:
continue
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if tabbed_browser.shutting_down:
@ -120,19 +123,37 @@ def buffer(*, info=None): # pylint: disable=unused-argument
tab.url().toDisplayString(),
tabbed_browser.page_title(idx)))
cat = listcategory.ListCategory("{}".format(win_id), tabs,
delete_func=delete_buffer)
delete_func=delete_buffer)
model.add_category(cat)
return model
def window(*, info=None): # pylint: disable=unused-argument
def buffer(*, info=None): # pylint: disable=unused-argument
"""A model to complete on open tabs across all windows.
Used for switching the buffer command.
"""
return _buffer()
def other_buffer(*, info):
"""A model to complete on open tabs across all windows except the current.
Used for the tab-take command.
"""
return _buffer(skip_win_id=info.win_id)
def window(*, info):
"""A model to complete on all open windows."""
model = completionmodel.CompletionModel(column_widths=(6, 30, 64))
windows = []
for win_id in objreg.window_registry:
if win_id == info.win_id:
continue
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tab_titles = (tab.title() for tab in tabbed_browser.widgets())

View File

@ -56,14 +56,17 @@ def url(*, info):
"""
model = completionmodel.CompletionModel(column_widths=(40, 50, 10))
quickmarks = ((url, name) for (name, url)
in objreg.get('quickmark-manager').marks.items())
quickmarks = [(url, name) for (name, url)
in objreg.get('quickmark-manager').marks.items()]
bookmarks = objreg.get('bookmark-manager').marks.items()
model.add_category(listcategory.ListCategory(
'Quickmarks', quickmarks, delete_func=_delete_quickmark, sort=False))
model.add_category(listcategory.ListCategory(
'Bookmarks', bookmarks, delete_func=_delete_bookmark, sort=False))
if quickmarks:
model.add_category(listcategory.ListCategory(
'Quickmarks', quickmarks, delete_func=_delete_quickmark,
sort=False))
if bookmarks:
model.add_category(listcategory.ListCategory(
'Bookmarks', bookmarks, delete_func=_delete_bookmark, sort=False))
if info.config.get('completion.web_history_max_items') != 0:
hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)

View File

@ -38,7 +38,7 @@ key_instance = None
change_filters = []
class change_filter: # pylint: disable=invalid-name
class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to filter calls based on a config section/option matching.
@ -104,13 +104,17 @@ class change_filter: # pylint: disable=invalid-name
if self._function:
@functools.wraps(func)
def wrapper(option=None):
"""Call the underlying function."""
if self._check_match(option):
return func()
return None
else:
@functools.wraps(func)
def wrapper(wrapper_self, option=None):
"""Call the underlying function."""
if self._check_match(option):
return func(wrapper_self)
return None
return wrapper
@ -162,10 +166,13 @@ class KeyConfig:
cmd_to_keys[cmd].insert(0, key)
return cmd_to_keys
def get_command(self, key, mode):
def get_command(self, key, mode, default=False):
"""Get the command for a given key (or None)."""
key = self._prepare(key, mode)
bindings = self.get_bindings_for(mode)
if default:
bindings = dict(val.bindings.default[mode])
else:
bindings = self.get_bindings_for(mode)
return bindings.get(key, None)
def bind(self, key, command, *, mode, save_yaml=False):
@ -257,7 +264,7 @@ class Config(QObject):
"""Set the given option to the given value."""
if not isinstance(objects.backend, objects.NoBackend):
if objects.backend not in opt.backends:
raise configexc.BackendError(objects.backend)
raise configexc.BackendError(opt.name, objects.backend)
opt.typ.to_py(value) # for validation
self._values[opt.name] = opt.typ.from_obj(value)
@ -458,7 +465,8 @@ class ConfigContainer:
def __setattr__(self, attr, value):
"""Set the given option in the config."""
if attr.startswith('_'):
return super().__setattr__(attr, value)
super().__setattr__(attr, value)
return
name = self._join(attr)
with self._handle_error('setting', name):

View File

@ -289,4 +289,7 @@ class ConfigCommands:
writer = configfiles.ConfigPyWriter(options, bindings,
commented=commented)
writer.write(filename)
try:
writer.write(filename)
except OSError as e:
raise cmdexc.CommandError(str(e))

View File

@ -174,6 +174,7 @@ def _parse_yaml_backends(name, node):
elif isinstance(node, dict):
return _parse_yaml_backends_dict(name, node)
_raise_invalid_node(name, 'backends', node)
raise utils.Unreachable
def _read_yaml(yaml_data):

View File

@ -34,6 +34,9 @@ history_gap_interval:
`:history`. Use -1 to disable separation.
ignore_case:
renamed: search.ignore_case
search.ignore_case:
type:
name: String
valid_values:
@ -43,6 +46,11 @@ ignore_case:
default: smart
desc: When to find text on a page case-insensitively.
search.incremental:
type: Bool
default: True
desc: Find text on a page incrementally, renewing the search for each typed character.
new_instance_open_target:
type:
name: String
@ -79,6 +87,9 @@ new_instance_open_target_window:
When `new_instance_open_target` is not set to `window`, this is ignored.
session_default_name:
renamed: session.default_name
session.default_name:
type:
name: SessionName
none_ok: true
@ -88,6 +99,11 @@ session_default_name:
If this is set to null, the session which was last loaded is saved.
session.lazy_restore:
type: Bool
default: false
desc: Load a restored tab as soon as it takes focus.
backend:
type:
name: String
@ -195,8 +211,10 @@ content.cache.size:
none_ok: true
minval: 0
maxval: maxint64
desc: Size (in bytes) of the HTTP network cache. Null to use the default
value.
desc: >-
Size (in bytes) of the HTTP network cache. Null to use the default value.
With QtWebEngine, the maximum supported value is 2147483647 (~2 GB).
# Defaults from QWebSettings::QWebSettings() in
# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp
@ -1233,6 +1251,11 @@ tabs.padding:
type: Padding
desc: Padding (in pixels) around text for tabs.
tabs.persist_mode_on_change:
default: false
type: Bool
desc: Stay in insert/passthrough mode when switching tabs.
tabs.position:
default: top
type: Position
@ -1285,6 +1308,7 @@ tabs.title.format:
- host
- private
- current_url
- protocol
none_ok: true
desc: |
Format to use for the tab title.
@ -1299,8 +1323,9 @@ tabs.title.format:
* `{scroll_pos}`: Page scroll position.
* `{host}`: Host of the current web page.
* `{backend}`: Either ''webkit'' or ''webengine''
* `{private}` : Indicates when private mode is enabled.
* `{current_url}` : URL of the current web page.
* `{private}`: Indicates when private mode is enabled.
* `{current_url}`: URL of the current web page.
* `{protocol}`: Protocol (http/https/...) of the current web page.
tabs.title.format_pinned:
default: '{index}'
@ -1317,6 +1342,7 @@ tabs.title.format_pinned:
- host
- private
- current_url
- protocol
none_ok: true
desc: Format to use for the tab title for pinned tabs. The same placeholders
like for `tabs.title.format` are defined.
@ -1460,6 +1486,7 @@ window.title_format:
- backend
- private
- current_url
- protocol
default: '{perc}{title}{title_sep}qutebrowser'
desc: |
Format to use for the window title. The same placeholders like for
@ -1513,9 +1540,15 @@ zoom.text_only:
## colors
colors.completion.fg:
default: white
type: QtColor
desc: Text color of the completion widget.
default: ["white", "white", "white"]
type:
name: ListOrValue
valtype: QtColor
desc: >-
Text color of the completion widget.
May be a single color to use for all columns or a list of three colors,
one for each column.
colors.completion.odd.bg:
default: '#444444'
@ -2269,8 +2302,8 @@ bindings.default:
command:
<Ctrl-P>: command-history-prev
<Ctrl-N>: command-history-next
<Up>: command-history-prev
<Down>: command-history-next
<Up>: completion-item-focus --history prev
<Down>: completion-item-focus --history next
<Shift-Tab>: completion-item-focus prev
<Tab>: completion-item-focus next
<Ctrl-Tab>: completion-item-focus next-category
@ -2280,6 +2313,7 @@ bindings.default:
<Ctrl-C>: completion-item-yank
<Ctrl-Shift-C>: completion-item-yank --sel
<Return>: command-accept
<Ctrl-Return>: command-accept --rapid
<Ctrl-B>: rl-backward-char
<Ctrl-F>: rl-forward-char
<Alt-B>: rl-backward-word

View File

@ -35,9 +35,9 @@ class BackendError(Error):
"""Raised when this setting is unavailable with the current backend."""
def __init__(self, backend):
super().__init__("This setting is not available with the {} "
"backend!".format(backend.name))
def __init__(self, name, backend):
super().__init__("The {} setting is not available with the {} "
"backend!".format(name, backend.name))
class ValidationError(Error):

View File

@ -136,7 +136,7 @@ class YamlConfig(QObject):
with open(self._filename, 'r', encoding='utf-8') as f:
yaml_data = utils.yaml_load(f)
except FileNotFoundError:
return {}
return
except OSError as e:
desc = configexc.ConfigErrorDesc("While reading", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])

View File

@ -66,14 +66,14 @@ def early_init(args):
configfiles.init()
objects.backend = get_backend(args)
for opt, val in args.temp_settings:
try:
config.instance.set_str(opt, val)
except configexc.Error as e:
message.error("set: {} - {}".format(e.__class__.__name__, e))
objects.backend = get_backend(args)
configtypes.Font.monospace_fonts = config.val.fonts.monospace
config.instance.changed.connect(_update_monospace_fonts)

View File

@ -499,7 +499,7 @@ class ListOrValue(BaseType):
_show_valtype = True
def __init__(self, valtype, none_ok=False, *args, **kwargs):
def __init__(self, valtype, *args, none_ok=False, **kwargs):
super().__init__(none_ok)
assert not isinstance(valtype, (List, ListOrValue)), valtype
self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs)
@ -963,7 +963,7 @@ class Font(BaseType):
# Gets set when the config is initialized.
monospace_fonts = None
font_regex = re.compile(r"""
^(
(
(
# style
(?P<style>normal|italic|oblique) |
@ -976,14 +976,14 @@ class Font(BaseType):
(?P<size>[0-9]+((\.[0-9]+)?[pP][tT]|[pP][xX]))
)\ # size/weight/style are space-separated
)* # 0-inf size/weight/style tags
(?P<family>.+)$ # mandatory font family""", re.VERBOSE)
(?P<family>.+) # mandatory font family""", re.VERBOSE)
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
if not self.font_regex.match(value): # pragma: no cover
if not self.font_regex.fullmatch(value): # pragma: no cover
# This should never happen, as the regex always matches everything
# as family.
raise configexc.ValidationError(value, "must be a valid font")
@ -1002,7 +1002,7 @@ class FontFamily(Font):
if not value:
return None
match = self.font_regex.match(value)
match = self.font_regex.fullmatch(value)
if not match: # pragma: no cover
# This should never happen, as the regex always matches everything
# as family.
@ -1039,7 +1039,7 @@ class QtFont(Font):
font.setStyle(QFont.StyleNormal)
font.setWeight(QFont.Normal)
match = self.font_regex.match(value)
match = self.font_regex.fullmatch(value)
if not match: # pragma: no cover
# This should never happen, as the regex always matches everything
# as family.

View File

@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block script %}
const STATE_BACK = "back";
const STATE_FORWARD = "forward";
function switch_state(new_state) {
history.replaceState(
new_state,
document.title,
location.pathname + location.hash);
}
function go_back() {
switch_state(STATE_FORWARD);
history.back();
}
function go_forward() {
switch_state(STATE_BACK);
history.forward();
}
function prepare_restore() {
if (!document.hidden) {
go_back();
return;
}
document.addEventListener("visibilitychange", go_back);
}
// there are three states
// default: register focus listener,
// on focus: go back and switch to the state forward
// back: user came from a later history entry
// -> switch to the state forward,
// forward him to the previous history entry
// forward: user came from a previous history entry
// -> switch to the state back,
// forward him to the next history entry
switch (history.state) {
case STATE_BACK:
go_back();
break;
case STATE_FORWARD:
go_forward();
break;
default:
setTimeout(prepare_restore, 1000);
break;
}
{% endblock %}
{% block content %}
<noscript><p>Javascript isn't enabled. So you need to manually go back in history to restore this tab.</p></noscript>
<p>Loading suspended page...<br>
<br>
If nothing happens, something went wrong or you disabled JavaScript.</p>
{% endblock %}

View File

@ -10,7 +10,6 @@ vim: ft=html fileencoding=utf-8 sts=4 sw=4 et:
<style type="text/css">
{% block style %}
body {
background-color: #fff;
margin: 0;
padding: 0;
}

View File

@ -16,7 +16,7 @@ body {
}
table {
border: 1px solid grey;
border: 1px solid #222;
border-collapse: collapse;
}
@ -25,7 +25,7 @@ pre {
}
th, td {
border: 1px solid grey;
border: 1px solid #222;
padding-left: 5px;
padding-right: 5px;
}

View File

@ -1,2 +1,4 @@
# Upstream Mozilla's code
pac_utils.js
# Actually a jinja template so eslint chokes on the {{}} syntax.
greasemonkey_wrapper.js

View File

@ -1,3 +1,15 @@
# qutebrowser's way of using eslint is perhaps a bit untypical: We turn on *all*
# the checks eslint has to offer, and then selectively disable/reconfigure the
# ones which got in the way below.
#
# This makes eslint much stricter (which is good). However, it means you might
# run into a case where you totally disagree with what it says, because some
# check is not useful or desired for qutebrowser, but nobody did run into it
# yet.
#
# In those cases, it's absolutely okay to modify this config as part of your PR.
# See it as a way to fine-tune eslint rather than a rigid style guide.
env:
browser: true
@ -41,3 +53,4 @@ rules:
no-multi-spaces: ["error", {"ignoreEOLComments": true}]
function-paren-newline: "off"
multiline-comment-style: "off"
no-bitwise: "off"

View File

@ -0,0 +1,118 @@
(function() {
const _qute_script_id = "__gm_" + {{ scriptName | tojson }};
function GM_log(text) {
console.log(text);
}
const GM_info = {
'script': {{ scriptInfo }},
'scriptMetaStr': {{ scriptMeta | tojson }},
'scriptWillUpdate': false,
'version': "0.0.1",
// so scripts don't expect exportFunction
'scriptHandler': 'Tampermonkey',
};
function checkKey(key, funcName) {
if (typeof key !== "string") {
throw new Error(`${funcName} requires the first parameter to be of type string, not '${typeof key}'`);
}
}
function GM_setValue(key, value) {
checkKey(key, "GM_setValue");
if (typeof value !== "string" &&
typeof value !== "number" &&
typeof value !== "boolean") {
throw new Error(`GM_setValue requires the second parameter to be of type string, number or boolean, not '${typeof value}'`);
}
localStorage.setItem(_qute_script_id + key, value);
}
function GM_getValue(key, default_) {
checkKey(key, "GM_getValue");
return localStorage.getItem(_qute_script_id + key) || default_;
}
function GM_deleteValue(key) {
checkKey(key, "GM_deleteValue");
localStorage.removeItem(_qute_script_id + key);
}
function GM_listValues() {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
if (localStorage.key(i).startsWith(_qute_script_id)) {
keys.push(localStorage.key(i).slice(_qute_script_id.length));
}
}
return keys;
}
function GM_openInTab(url) {
window.open(url);
}
// Almost verbatim copy from Eric
function GM_xmlhttpRequest(/* object */ details) {
details.method = details.method ? details.method.toUpperCase() : "GET";
if (!details.url) {
throw new Error("GM_xmlhttpRequest requires an URL.");
}
// build XMLHttpRequest object
const oXhr = new XMLHttpRequest();
// run it
if ("onreadystatechange" in details) {
oXhr.onreadystatechange = function() {
details.onreadystatechange(oXhr);
};
}
if ("onload" in details) {
oXhr.onload = function() { details.onload(oXhr); };
}
if ("onerror" in details) {
oXhr.onerror = function () { details.onerror(oXhr); };
}
oXhr.open(details.method, details.url, true);
if ("headers" in details) {
for (const header in details.headers) {
oXhr.setRequestHeader(header, details.headers[header]);
}
}
if ("data" in details) {
oXhr.send(details.data);
} else {
oXhr.send();
}
}
function GM_addStyle(/* String */ styles) {
const oStyle = document.createElement("style");
oStyle.setAttribute("type", "text/css");
oStyle.appendChild(document.createTextNode(styles));
const head = document.getElementsByTagName("head")[0];
if (head === undefined) {
document.onreadystatechange = function() {
if (document.readyState === "interactive") {
document.getElementsByTagName("head")[0].appendChild(oStyle);
}
};
} else {
head.appendChild(oStyle);
}
}
const unsafeWindow = window;
// ====== The actual user script source ====== //
{{ scriptSource }}
// ====== End User Script ====== //
})();

View File

@ -0,0 +1,136 @@
/**
* Copyright 2017 Ulrik de Muelenaere <ulrikdem@gmail.com>
*
* This file is part of qutebrowser.
*
* qutebrowser is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* qutebrowser is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
window._qutebrowser.stylesheet = (function() {
if (window._qutebrowser.stylesheet) {
return window._qutebrowser.stylesheet;
}
const funcs = {};
const xhtml_ns = "http://www.w3.org/1999/xhtml";
const svg_ns = "http://www.w3.org/2000/svg";
let root_elem;
let style_elem;
let css_content = "";
let root_observer;
let initialized = false;
// Watch for rewrites of the root element and changes to its children,
// then move the stylesheet to the end. Partially inspired by Stylus:
// https://github.com/openstyles/stylus/blob/1.1.4.2/content/apply.js#L235-L355
function watch_root() {
if (root_elem !== document.documentElement) {
root_elem = document.documentElement;
root_observer.disconnect();
root_observer.observe(document, {"childList": true});
root_observer.observe(root_elem, {"childList": true});
}
if (style_elem !== root_elem.lastChild) {
root_elem.appendChild(style_elem);
}
}
function create_style() {
let ns = xhtml_ns;
if (document.documentElement.namespaceURI === svg_ns) {
ns = svg_ns;
}
style_elem = document.createElementNS(ns, "style");
style_elem.textContent = css_content;
root_observer = new MutationObserver(watch_root);
watch_root();
}
// We should only inject the stylesheet if the document already has style
// information associated with it. Otherwise we wait until the browser
// rewrites it to an XHTML document showing the document tree. As a
// starting point for exploring the relevant code in Chromium, see
// https://github.com/qt/qtwebengine-chromium/blob/cfe8c60/chromium/third_party/WebKit/Source/core/xml/parser/XMLDocumentParser.cpp#L1539-L1540
function check_style(node) {
const stylesheet = node.nodeType === Node.PROCESSING_INSTRUCTION_NODE &&
node.target === "xml-stylesheet" &&
node.parentNode === document;
const known_ns = node.nodeType === Node.ELEMENT_NODE &&
(node.namespaceURI === xhtml_ns ||
node.namespaceURI === svg_ns);
return stylesheet || known_ns;
}
function init() {
initialized = true;
// Chromium will not rewrite a document inside a frame, so add the
// stylesheet even if the document is unstyled.
if (window !== window.top) {
create_style();
return;
}
const iter = document.createNodeIterator(document,
NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_ELEMENT);
let node;
while ((node = iter.nextNode())) {
if (check_style(node)) {
create_style();
return;
}
}
const style_observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
const nodes = mutation.addedNodes;
for (let i = 0; i < nodes.length; ++i) {
if (check_style(nodes[i])) {
create_style();
style_observer.disconnect();
return;
}
}
}
});
style_observer.observe(document, {"childList": true, "subtree": true});
}
funcs.set_css = function(css) {
if (!initialized) {
init();
}
if (style_elem) {
style_elem.textContent = css;
// The browser seems to rewrite the document in same-origin frames
// without notifying the mutation observer. Ensure that the
// stylesheet is in the current document.
watch_root();
} else {
css_content = css;
}
// Propagate the new CSS to all child frames.
// FIXME:qtwebengine This does not work for cross-origin frames.
for (let i = 0; i < window.frames.length; ++i) {
const frame = window.frames[i];
if (frame._qutebrowser && frame._qutebrowser.stylesheet) {
frame._qutebrowser.stylesheet.set_css(css);
}
}
};
return funcs;
})();

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