Merge remote-tracking branch 'upstream/master' into jay/frame-hinting

This commit is contained in:
Jay Kamat 2018-01-17 17:24:43 -05:00
commit 12d729c3bc
No known key found for this signature in database
GPG Key ID: 5D2E399600F4F7B5
207 changed files with 3090 additions and 1340 deletions

View File

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

View File

@ -1,6 +1,7 @@
[flake8] [flake8]
exclude = .*,__pycache__,resources.py exclude = .*,__pycache__,resources.py
# B001: bare except # B001: bare except
# B008: Do not perform calls in argument defaults. (fine with some Qt stuff)
# B305: .next() (false-positives) # B305: .next() (false-positives)
# E128: continuation line under-indented for visual indent # E128: continuation line under-indented for visual indent
# E226: missing whitespace around arithmetic operator # E226: missing whitespace around arithmetic operator
@ -33,7 +34,7 @@ exclude = .*,__pycache__,resources.py
# D413: Missing blank line after last section (not in pep257?) # D413: Missing blank line after last section (not in pep257?)
# A003: Builtin name for class attribute (needed for attrs) # A003: Builtin name for class attribute (needed for attrs)
ignore = ignore =
B001,B305, B001,B008,B305,
E128,E226,E265,E501,E402,E266,E722,E731, E128,E226,E265,E501,E402,E266,E722,E731,
F401, F401,
N802, N802,

16
.gitignore vendored
View File

@ -31,12 +31,12 @@ __pycache__
/prof /prof
/venv /venv
TODO TODO
/scripts/testbrowser_cpp/webkit/Makefile /scripts/testbrowser/cpp/webkit/Makefile
/scripts/testbrowser_cpp/webkit/main.o /scripts/testbrowser/cpp/webkit/main.o
/scripts/testbrowser_cpp/webkit/testbrowser /scripts/testbrowser/cpp/webkit/testbrowser
/scripts/testbrowser_cpp/webkit/.qmake.stash /scripts/testbrowser/cpp/webkit/.qmake.stash
/scripts/testbrowser_cpp/webengine/Makefile /scripts/testbrowser/cpp/webengine/Makefile
/scripts/testbrowser_cpp/webengine/main.o /scripts/testbrowser/cpp/webengine/main.o
/scripts/testbrowser_cpp/webengine/testbrowser /scripts/testbrowser/cpp/webengine/testbrowser
/scripts/testbrowser_cpp/webengine/.qmake.stash /scripts/testbrowser/cpp/webengine/.qmake.stash
/scripts/dev/pylint_checkers/qute_pylint.egg-info /scripts/dev/pylint_checkers/qute_pylint.egg-info

View File

@ -13,38 +13,33 @@ persistent=n
[MESSAGES CONTROL] [MESSAGES CONTROL]
enable=all enable=all
disable=no-self-use, disable=locally-disabled,
fixme,
global-statement,
locally-disabled,
locally-enabled, locally-enabled,
too-many-ancestors, suppressed-message,
too-few-public-methods, fixme,
too-many-public-methods, no-self-use,
cyclic-import, cyclic-import,
bad-continuation,
too-many-instance-attributes,
blacklisted-name, blacklisted-name,
too-many-lines,
logging-format-interpolation, logging-format-interpolation,
logging-not-lazy,
broad-except, broad-except,
bare-except, bare-except,
eval-used, eval-used,
exec-used, exec-used,
ungrouped-imports, global-statement,
suppressed-message,
too-many-return-statements,
duplicate-code,
wrong-import-position, wrong-import-position,
duplicate-code,
no-else-return, no-else-return,
# https://github.com/PyCQA/pylint/issues/1698 too-many-ancestors,
unsupported-membership-test, too-many-public-methods,
unsupported-assignment-operation, too-many-instance-attributes,
unsubscriptable-object, too-many-lines,
too-many-return-statements,
too-many-boolean-expressions, too-many-boolean-expressions,
too-many-locals, too-many-locals,
too-many-branches, too-many-branches,
too-many-statements too-many-statements,
too-few-public-methods
[BASIC] [BASIC]
function-rgx=[a-z_][a-z0-9_]{2,50}$ function-rgx=[a-z_][a-z0-9_]{2,50}$
@ -73,10 +68,10 @@ valid-metaclass-classmethod-first-arg=cls
[TYPECHECK] [TYPECHECK]
ignored-modules=PyQt5,PyQt5.QtWebKit ignored-modules=PyQt5,PyQt5.QtWebKit
ignored-classes=_CountingAttr
[IMPORTS] [IMPORTS]
# WORKAROUND # WORKAROUND
# For some reason, pylint doesn't know about some Python 3 modules on # For some reason, pylint doesn't know about some Python 3 modules on
# AppVeyor... # AppVeyor...
known-standard-library=faulthandler,http,enum,tokenize,posixpath,importlib,types 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 env: TESTENV=py36-pyqt59-cov
- os: osx - os: osx
env: TESTENV=py36 OSX=sierra env: TESTENV=py36 OSX=sierra
osx_image: xcode8.3 osx_image: xcode9.2
language: generic language: generic
# https://github.com/qutebrowser/qutebrowser/issues/2013 # https://github.com/qutebrowser/qutebrowser/issues/2013
# - os: osx # - os: osx
@ -52,6 +52,10 @@ matrix:
language: node_js language: node_js
python: null python: null
node_js: "lts/*" node_js: "lts/*"
- os: linux
language: generic
env: TESTENV=shellcheck
services: docker
fast_finish: true fast_finish: true
cache: cache:

View File

@ -23,7 +23,7 @@ include qutebrowser/config/configdata.yml
prune www prune www
prune scripts/dev prune scripts/dev
prune scripts/testbrowser_cpp prune scripts/testbrowser/cpp
prune .github prune .github
exclude scripts/asciidoc2html.py exclude scripts/asciidoc2html.py
exclude doc/notes exclude doc/notes

View File

@ -15,57 +15,105 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes. // `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities. // `Security` to invite users to upgrade in case of vulnerabilities.
v1.1.0 (unreleased) v1.2.0 (unreleased)
------------------- -------------------
Added
~~~~~
- 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
completion less often.
- New `completion.use_best_match` setting to automatically use the best-matching
command in the completion.
- New `:tab-give` and `:tab-take` commands, to give tabs to another window, or
take them from another window.
- New `config.source(...)` method for `config.py` to source another file.
- New `keyhint.radius` option to configure the edge rounding for the key hint
widget.
- `:edit-url` now handles the `--private` and `--related` flags, which have the
same effect they have with `:open`.
- New `{line}` and `{column}` replacements for `editor.command` to position the
cursor correctly.
- New `qute-pass` userscript as alternative to `password_fill` which allows
selecting accounts via rofi or any other dmenu-compatile application.
- New `qt.highdpi` setting to turn on Qt's High-DPI scaling.
- New `:completion-item-yank` command (bound to `<Ctrl-C>`) to yank the current
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
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.
Changed Changed
~~~~~~~ ~~~~~~~
- Some tabs settings got renamed: - The `hist_importer.py` script now only imports URL schemes qutebrowser can
handle.
- Deleting a prefix (`:`, `/` or `?`) via backspace now leaves command mode.
- Angular 1 elements now get hints assigned.
- `:tab-only` with pinned tabs now still closes unpinned tabs.
Fixed
~~~~~
- Improved fullscreen handling with Qt 5.10.
v1.1.1 (unreleased)
-------------------
Fixed
~~~~~
- The Makefile now actually works.
v1.1.0
------
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 fields for `window.title_format` and `tabs.title.format`:
* `{current_url}`
* `{protocol}`
- New settings:
* `colors.statusbar.passthrough.fg`/`.bg`
* `completion.delay` and `completion.min_chars` to update the completion less
often.
* `completion.use_best_match` to automatically use the best-matching
command in the completion.
* `keyhint.radius` to configure the edge rounding for the key hint widget.
* `qt.highdpi` to turn on Qt's High-DPI scaling.
* `tabs.pinned.shrink` (`true` by default) to make it possible
for pinned tabs and normal tabs to have the same size.
* `content.windowed_fullscreen` to show e.g. a fullscreened video in the
window without fullscreening that window.
* `tabs.persist_mode_on_change` to keep the current mode when
switching tabs.
* `session.lazy_restore` which allows to not load pages immediately
when restoring a session.
- New commands:
* `:tab-give` and `:tab-take`, to give tabs to another window, or take them
from another window.
* `:completion-item-yank` (bound to `<Ctrl-C>`) to yank the current
completion item text.
* `:edit-command` to edit the commandline in an editor.
* `search.incremental` for incremental text search.
- New flags for existing commands:
* `-o` flag for `:spawn` to show stdout/stderr in a new tab.
* `--rapid` flag for `:command-accept` (bound to `Ctrl-Enter` by default),
which allows executing a command in the completion without closing it.
* `--private` and `--related` flags for `:edit-url`, which have the
same effect they have with `:open`.
* `--history` for `:completion-item-focus` 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.
* `--file` for `:debug-pyeval` which makes it take a filename instead of a
line of code.
- New `config.source(...)` method for `config.py` to source another file.
- New `{line}` and `{column}` replacements for `editor.command` to position the
cursor correctly.
- New `qute-pass` userscript as alternative to `password_fill` which allows
selecting accounts via rofi or any other dmenu-compatile application.
- New `hist_importer.py` script to import history from Firefox/Chromium.
Changed
~~~~~~~
- Some settings got renamed:
* `tabs.width.bar` -> `tabs.width` * `tabs.width.bar` -> `tabs.width`
* `tabs.width.indicator` -> `tabs.indicator.width` * `tabs.width.indicator` -> `tabs.indicator.width`
* `tabs.indicator_padding` -> `tabs.indicator.padding` * `tabs.indicator_padding` -> `tabs.indicator.padding`
* `session_default_name` -> `session.default_name`
* `ignore_case` -> `search.ignore_case`
- Much improved user stylesheet handling for QtWebEngine which reduces
flickering and updates immediately after setting a stylesheet.
- High-DPI favicons are now used when available. - High-DPI favicons are now used when available.
- The `asciidoc2html.py` script now uses Pygments (which is already a dependency - The `asciidoc2html.py` script now uses Pygments (which is already a dependency
of qutebrowser) instead of `source-highlight` for syntax highlighting. of qutebrowser) instead of `source-highlight` for syntax highlighting.
- The `:buffer` command now doesn't require quoting anymore, similar to `:open`. - The `:buffer` command now doesn't require quoting anymore, similar to `:open`.
- The `importer.py` script was largely rewritten and now also supports importing - The `importer.py` script was largely rewritten and now also supports importing
from Firefox' `places.sqlite` file and Chrome/Chromium profiles. from Firefox' `places.sqlite` file and Chrome/Chromium profiles.
- Various internal refactorings to use Python 3.5 and ECMAscript 6 features - Various internal refactorings to use Python 3.5 and ECMAscript 6 features.
- If the `window.hide_wayland_decoration` setting is False, but - If the `window.hide_wayland_decoration` setting is False, but
`QT_WAYLAND_DISABLE_WINDOWDECORATION` is set in the environment, `QT_WAYLAND_DISABLE_WINDOWDECORATION` is set in the environment,
the decorations are still hidden. the decorations are still hidden.
@ -77,36 +125,46 @@ Changed
- The `qute://version` page now also shows the uptime of qutebrowser. - The `qute://version` page now also shows the uptime of qutebrowser.
- qutebrowser now prompts to create a non-existing directory when starting a - qutebrowser now prompts to create a non-existing directory when starting a
download. download.
- Much improved user stylesheet handling which reduces flickering - `:jseval --file` now searches relative paths in a `js/` subdir in
and updates immediately after setting a stylesheet. qutebrowser's data dir, e.g. `~/.local/share/qutebrowser/js`.
- `:completion-item-focus` now has a `--history` flag which causes it to go - The current/default bindings are now shown in the ``:bind` completion.
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. - 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.
- qutebrowser now ensures it's focused again after an external editor is closed.
- The `colors.completion.fg` setting can now be a list, allowing to specify
different colors for the three completion columns.
Fixed Fixed
~~~~~ ~~~~~
- More consistent sizing for favicons with vertical tabs. - More consistent sizing for favicons with vertical tabs.
- Using `:home` on pinned tabs is now prevented. - Using `:home` on pinned tabs is now prevented.
- Fix crash with unknown file types loaded via qute://help. - Fix crash with unknown file types loaded via `qute://help`.
- Scrolling performance improvements. - Scrolling performance improvements.
- Sites like `qute://help` now redirect to `qute://help/` to make sure links - Sites like `qute://help` now redirect to `qute://help/` to make sure links
work properly. work properly.
- Fixes for the size calculation of pinned tabs in the tab bar. - Fixes for the size calculation of pinned tabs in the tab bar.
- Worked around a crash with PyQt 5.9.1 compiled against Qt < 5.9.1 when using - Worked around a crash with PyQt 5.9.1 compiled against Qt < 5.9.1 when using
:yank or qute:// URLs. `:yank` or `qute://` URLs.
- Fixed crash when opening `qute://help/img` - Fixed crash when opening `qute://help/img`.
- Fixed `gU` (`:navigate up`) on `qute://help` and webservers not handling `..` - Fixed `gU` (`:navigate up`) on `qute://help` and webservers not handling `..`
in a URL. in a URL.
- Using e.g. `-s backend webkit` to set the backend now works correctly. - 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. - 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.
- Fixed a crash when writing a flag before a command (e.g. `:-w open `).
- Fixed a crash when clicking certain form elements with QtWebEngine.
Deprecated Deprecated
~~~~~~~~~~ ~~~~~~~~~~
@ -119,6 +177,8 @@ Removed
- The long-deprecated `:prompt-yes`, `:prompt-no`, `:paste-primary` and `:paste` - The long-deprecated `:prompt-yes`, `:prompt-no`, `:paste-primary` and `:paste`
commands have been removed. 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 `messages.unfocused` option which wasn't used anymore was removed.
- The `x[xtb]` default bindings got removed again as many users accidentally - The `x[xtb]` default bindings got removed again as many users accidentally
triggered them. triggered them.

View File

@ -221,5 +221,5 @@ My issue is not listed.::
https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or
using the `:report` command. using the `:report` command.
If you are reporting a segfault, make sure you read the 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. information.

View File

@ -54,6 +54,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<follow-selected,follow-selected>>|Follow the selected text. |<<follow-selected,follow-selected>>|Follow the selected text.
|<<forward,forward>>|Go forward in the history of the current tab. |<<forward,forward>>|Go forward in the history of the current tab.
|<<fullscreen,fullscreen>>|Toggle fullscreen mode. |<<fullscreen,fullscreen>>|Toggle fullscreen mode.
|<<greasemonkey-reload,greasemonkey-reload>>|Re-read Greasemonkey scripts from disk.
|<<help,help>>|Show help about a command or setting. |<<help,help>>|Show help about a command or setting.
|<<hint,hint>>|Start hinting. |<<hint,hint>>|Start hinting.
|<<history,history>>|Show browsing history. |<<history,history>>|Show browsing history.
@ -333,12 +334,10 @@ Write the current configuration to a config.py file.
[[download]] [[download]]
=== 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. 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 ==== positional arguments
* +'url'+: The URL to download. If not given, download the current page. * +'url'+: The URL to download. If not given, download the current page.
@ -491,6 +490,12 @@ Toggle fullscreen mode.
==== optional arguments ==== optional arguments
* +*-l*+, +*--leave*+: Only leave fullscreen if it was entered by the page. * +*-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]]
=== help === help
Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+ Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
@ -511,7 +516,7 @@ Show help about a command or setting.
[[hint]] [[hint]]
=== hint === hint
Syntax: +:hint [*--rapid*] [*--mode* 'mode'] [*--add-history*] Syntax: +:hint [*--mode* 'mode'] [*--add-history*] [*--rapid*]
['group'] ['target'] ['args' ['args' ...]]+ ['group'] ['target'] ['args' ['args' ...]]+
Start hinting. Start hinting.
@ -565,11 +570,6 @@ Start hinting.
==== optional arguments ==== 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. * +*-m*+, +*--mode*+: The hinting mode to use.
- `number`: Use numeric hints. - `number`: Use numeric hints.
@ -581,6 +581,11 @@ Start hinting.
* +*-a*+, +*--add-history*+: Whether to add the spawned or yanked link to the browsing history. * +*-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 ==== note
* This command does not split arguments after the last argument and handles quotes literally. * This command does not split arguments after the last argument and handles quotes literally.
@ -1083,7 +1088,7 @@ Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-win
Save a session. Save a session.
==== positional arguments ==== 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 ==== optional arguments
@ -1141,7 +1146,7 @@ Set a mark at the current scroll position in the current tab.
[[spawn]] [[spawn]]
=== spawn === spawn
Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+ Syntax: +:spawn [*--userscript*] [*--verbose*] [*--output*] [*--detach*] 'cmdline'+
Spawn a command in a shell. Spawn a command in a shell.
@ -1156,6 +1161,7 @@ Spawn a command in a shell.
- `/usr/share/qutebrowser/userscripts` - `/usr/share/qutebrowser/userscripts`
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited. * +*-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. * +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
==== note ==== note
@ -1415,8 +1421,13 @@ How many steps to zoom out.
|============== |==============
[[command-accept]] [[command-accept]]
=== command-accept === command-accept
Syntax: +:command-accept [*--rapid*]+
Execute the command currently in the commandline. 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]]
=== command-history-next === command-history-next
Go forward in the commandline history. Go forward in the commandline history.

View File

@ -264,7 +264,7 @@ get a string:
.config.py: .config.py:
[source,python] [source,python]
---- ----
print(str(config.configdir / 'config.py') print(str(config.configdir / 'config.py'))
---- ----
Handling errors Handling errors

View File

@ -199,7 +199,6 @@
|<<hints.scatter,hints.scatter>>|Scatter hint key chains (like Vimium) or not (like dwb). |<<hints.scatter,hints.scatter>>|Scatter hint key chains (like Vimium) or not (like dwb).
|<<hints.uppercase,hints.uppercase>>|Make characters in hint strings uppercase. |<<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. |<<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.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_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. |<<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. |<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling.
|<<scrolling.bar,scrolling.bar>>|Show a scrollbar. |<<scrolling.bar,scrolling.bar>>|Show a scrollbar.
|<<scrolling.smooth,scrolling.smooth>>|Enable smooth scrolling for web pages. |<<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. |<<spellcheck.languages,spellcheck.languages>>|Languages to use for spell checking.
|<<statusbar.hide,statusbar.hide>>|Hide the statusbar unless a message is shown. |<<statusbar.hide,statusbar.hide>>|Hide the statusbar unless a message is shown.
|<<statusbar.padding,statusbar.padding>>|Padding (in pixels) for the statusbar. |<<statusbar.padding,statusbar.padding>>|Padding (in pixels) for the statusbar.
@ -425,6 +427,7 @@ Default:
* +pass:[&lt;Ctrl-K&gt;]+: +pass:[rl-kill-line]+ * +pass:[&lt;Ctrl-K&gt;]+: +pass:[rl-kill-line]+
* +pass:[&lt;Ctrl-N&gt;]+: +pass:[command-history-next]+ * +pass:[&lt;Ctrl-N&gt;]+: +pass:[command-history-next]+
* +pass:[&lt;Ctrl-P&gt;]+: +pass:[command-history-prev]+ * +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-C&gt;]+: +pass:[completion-item-yank --sel]+
* +pass:[&lt;Ctrl-Shift-Tab&gt;]+: +pass:[completion-item-focus prev-category]+ * +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-Tab&gt;]+: +pass:[completion-item-focus next-category]+
@ -698,10 +701,15 @@ Default: +pass:[#333333]+
[[colors.completion.fg]] [[colors.completion.fg]]
=== colors.completion.fg === colors.completion.fg
Text color of the completion widget. 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]]
=== colors.completion.item.selected.bg === colors.completion.item.selected.bg
@ -2325,20 +2333,6 @@ Type: <<types,Int>>
Default: +pass:[30]+ 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]]
=== input.forward_unbound_keys === input.forward_unbound_keys
Which unbound keys to forward to the webview in normal mode. Which unbound keys to forward to the webview in normal mode.
@ -2556,8 +2550,30 @@ Type: <<types,Bool>>
Default: +pass:[false]+ Default: +pass:[false]+
[[session_default_name]] [[search.ignore_case]]
=== session_default_name === 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. Name of the session to save by default.
If this is set to null, the session which was last loaded is saved. If this is set to null, the session which was last loaded is saved.
@ -2565,6 +2581,14 @@ Type: <<types,SessionName>>
Default: empty 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]]
=== spellcheck.languages === spellcheck.languages
Languages to use for spell checking. Languages to use for spell checking.
@ -2906,6 +2930,7 @@ The following placeholders are defined:
* `{backend}`: Either ''webkit'' or ''webengine'' * `{backend}`: Either ''webkit'' or ''webengine''
* `{private}`: Indicates when private mode is enabled. * `{private}`: Indicates when private mode is enabled.
* `{current_url}`: URL of the current web page. * `{current_url}`: URL of the current web page.
* `{protocol}`: Protocol (http/https/...) of the current web page.
Type: <<types,FormatString>> 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. 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 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. https://github.com/pyenv/pyenv[pyenv], but nobody tried that yet.
If you get qutebrowser running on those distributions, please 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 QtWebEngine). However, it comes with Python 3.5, so you can
<<tox,install qutebrowser via tox>>. <<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 Those versions come with QtWebEngine in the repositories. This makes it possible
@ -54,7 +54,18 @@ Install the packages:
# apt install ./qutebrowser_*_all.deb # 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 - Alternatively, you can <<tox,install qutebrowser via tox>> to get a newer
QtWebEngine version. QtWebEngine version.
@ -67,8 +78,7 @@ $ python3 scripts/asciidoc2html.py
---- ----
- If you prefer using QtWebKit, there's an up-to-date version available in - 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] https://packages.debian.org/buster/libqt5webkit5[Debian Testing].
for Debian Stretch.
- If video or sound don't work with QtWebKit, try installing the gstreamer plugins: - If video or sound don't work with QtWebKit, try installing the gstreamer plugins:
+ +
---- ----
@ -90,6 +100,18 @@ qutebrowser is available in the official repositories:
However, note that Fedora 25/26 won't be updated to qutebrowser v1.0, so you 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. 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 On Archlinux
------------ ------------
@ -379,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 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. 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 On Windows, run `set PYTHON=C:\path\to\python.exe` (CMD) or ``$Env:PYTHON =
Python3 is in your PATH before running tox. "..."` (Powershell) first.
Creating a wrapper script Creating a wrapper script
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -1,207 +1,302 @@
/* XPM */ /* XPM */
static char *qutebrowser[] = { static char * qutebrowser_xpm[] = {
/* columns rows colors chars-per-pixel */ "32 32 267 2",
"32 32 169 2 ", " c None",
" c #0A396E", ". c #9FD4FD",
". c #0B3C72", "+ c #99CBFE",
"X c #0B4077", "@ c #90C3FE",
"o c #0C437B", "# c #89BFFE",
"O c #134175", "$ c #81BCFF",
"+ c #15467C", "% c #80BBFF",
"@ c #18477B", "& c #9BCAFD",
"# c #1A497D", "* c #A9DBFB",
"$ c #0D4B86", "= c #88D3FB",
"% c #0F4E8D", "- c #98CBFE",
"& c #124A80", "; c #81BBFF",
"* c #1F4F83", "> c #7EBAFF",
"= c #0E518C", ", c #84BDFF",
"- c #1F5084", "' c #8DC2FF",
"; c #11508C", ") c #96C7FE",
": c #0F5193", "! c #A0CCFE",
"> c #115799", "~ c #A9D1FE",
", c #115B9C", "{ c #CEE5FD",
"< c #204F83", "] c #C7E3FC",
"1 c #245287", "^ c #8AD3FB",
"2 c #2A598C", "/ c #9DCFFD",
"3 c #325E8F", "( c #C3DFFD",
"4 c #11609F", "_ c #CDE4FD",
"5 c #346496", ": c #A3CEFE",
"6 c #3B6898", "< c #94C6FE",
"7 c #115CA1", "[ c #CAE5FC",
"8 c #115EAC", "} c #7DD0FB",
"9 c #1263A3", "| c #9ECDFD",
"0 c #1260AD", "1 c #A1CDFE",
"q c #136BAC", "2 c #8BC1FF",
"w c #136BB2", "3 c #87BFFF",
"e c #1366BA", "4 c #ADD4FE",
"r c #196BB2", "5 c #C6E1FD",
"t c #157ABB", "6 c #CCE3FC",
"y c #1577BB", "7 c #A7DAFB",
"u c #2E6DB0", "8 c #9DCBFE",
"i c #387FB1", "9 c #78AFF1",
"p c #456E9A", "0 c #6096D4",
"a c #4873A1", "a c #4B82C0",
"s c #4375AA", "b c #5A84B3",
"d c #507AA6", "c c #6589B1",
"f c #597EA4", "d c #6F92B9",
"g c #4D7EB3", "e c #90AED0",
"h c #156FCB", "f c #C4DBF5",
"j c #167AC5", "g c #6286AE",
"k c #1675CA", "h c #7D9EC2",
"l c #177BCE", "i c #BADFFC",
"z c #1777D8", "j c #85BDFE",
"x c #1476E4", "k c #78B4F8",
"c c #167BE6", "l c #4C83C0",
"v c #167DE8", "m c #1E4F87",
"b c #197EEF", "n c #0A396E",
"n c #1A7FF0", "o c #345D8D",
"m c #1A80BE", "p c #CDE4FC",
"M c #5F87AF", "q c #88A7CA",
"N c #5D8BBA", "r c #1D497C",
"B c #5A84B1", "s c #799BBF",
"V c #6C8FB3", "t c #8AC1FD",
"C c #6F96BE", "u c #5E97D7",
"Z c #1886CC", "v c #14457B",
"A c #1883D7", "w c #4F76A0",
"S c #198DD5", "x c #A9D5FC",
"D c #1987D9", "y c #95C9FD",
"F c #198ADC", "z c #4C82C1",
"G c #1A96DC", "A c #0A3A6F",
"H c #3090D9", "B c #C9E3FD",
"J c #1682E9", "C c #95CCFC",
"K c #1983ED", "D c #629BDB",
"L c #1689E9", "E c #0B3A6F",
"P c #1A8DEE", "F c #0C3B6F",
"I c #1B95ED", "G c #4E749F",
"U c #1C9EEA", "H c #8CACCE",
"Y c #1B97E4", "I c #6185AD",
"T c #1A84F2", "J c #CBE4FD",
"R c #1A8BF2", "K c #89C0FF",
"E c #1C94F4", "L c #98CDFA",
"W c #1D9CF5", "M c #27558A",
"Q c #3388E6", "N c #144175",
"! c #3D90E9", "O c #9BB8D8",
"~ c #228EF3", "P c #335D8C",
"^ c #229FF6", "Q c #AFC9E6",
"/ c #3294F4", "R c #AFD4FE",
"( c #3D9FF6", "S c #91C7FD",
") c #339CF4", "T c #A0C0DE",
"_ c #1CA2E5", "U c #194779",
"` c #1DABEE", "V c #80A1C5",
"' c #1DA4F6", "W c #C8E1F9",
"] c #1EA9F7", "X c #9CB9D8",
"[ c #1EADF8", "Y c #7799BE",
"{ c #1FB4F9", "Z c #6489B0",
"} c #1FB9FA", "` c #7092B9",
"| c #20ACF8", " . c #6E9DCF",
" . c #27A4F6", ".. c #79B5F9",
".. c #3DA9F6", "+. c #83BDFE",
"X. c #20B9FA", "@. c #7395BA",
"o. c #2EB6F9", "#. c #315C8B",
"O. c #458DC9", "$. c #7C9EC2",
"+. c #5C8DC1", "%. c #C0D9F3",
"@. c #5795C6", "&. c #7294BA",
"#. c #709DCB", "*. c #5C94D4",
"$. c #74A8DD", "=. c #91CCFC",
"%. c #4A97EA", "-. c #88CBFA",
"&. c #4896EA", ";. c #5179A3",
"*. c #559EEA", ">. c #6E91B7",
"=. c #439AF5", ",. c #6084AC",
"-. c #46A3F6", "'. c #96B3D4",
";. c #5FA9F6", "). c #275283",
":. c #5EA6F3", "!. c #0C3C71",
">. c #47BCF9", "~. c #629CDC",
",. c #51B5F8", "{. c #94C6FD",
"<. c #58BDF8", "]. c #A7D2FC",
"1. c #68ABEF", "^. c #36659A",
"2. c #7DB9E7", "/. c #2C5788",
"3. c #63AEF7", "(. c #9DBAD9",
"4. c #6FB1F7", "_. c #B4CEEA",
"5. c #66B9F8", ":. c #476E9A",
"6. c #61B2F6", "<. c #7EB9FE",
"7. c #71B4F7", "[. c #8DC3FD",
"8. c #78B7F4", "}. c #8CC2FE",
"9. c #72BFF9", "|. c #2F619B",
"0. c #3BC0FA", "1. c #87A6C9",
"q. c #6FCEFB", "2. c #7A9BC0",
"w. c #6CC5FA", "3. c #CBE2FB",
"e. c #7BCAF9", "4. c #C7DFF8",
"r. c #89A7C3", "5. c #6C8FB5",
"t. c #83A2C1", "6. c #113F73",
"y. c #98B6D3", "7. c #0F3D71",
"u. c #9DB9D3", "8. c #547AA4",
"i. c #89B6E4", "9. c #9CBAD9",
"p. c #83B6E9", "0. c #B9D3EE",
"a. c #81BDF7", "a. c #A3C0DE",
"s. c #83BFF8", "b. c #31629A",
"d. c #9EC4E9", "c. c #659EE0",
"f. c #8CC2F9", "d. c #87BFFE",
"g. c #85CDFB", "e. c #C3E0FD",
"h. c #87C4F9", "f. c #4371A4",
"j. c #92C6F9", "g. c #7496BB",
"k. c #95CAFA", "h. c #90AFD1",
"l. c #9CCBFA", "i. c #245081",
"z. c #89D7FC", "j. c #416A96",
"x. c #91D9FC", "k. c #B0CBE7",
"c. c #9CDEFD", "l. c #CCE4FD",
"v. c #9ED2FB", "m. c #7DB8FD",
"b. c #A7CAEC", "n. c #1E5088",
"n. c #B5CEE3", "o. c #497EBC",
"m. c #A1CEFA", "p. c #C9E3FC",
"M. c #AED0F0", "q. c #7193B9",
"N. c #ACD6FA", "r. c #C6E0FB",
"B. c #A0DFFC", "s. c #A2CDFE",
"V. c #AFD8FC", "t. c #97C8FE",
"C. c #B5D9FB", "u. c #A7D0FE",
"Z. c #BCDDFC", "v. c #BDDCFD",
"A. c #BFDCF5", "w. c #9EC2E8",
"S. c #ACE3FD", "x. c #416996",
"D. c #B5E5FE", "y. c #366AA6",
"F. c #BBE2FC", "z. c #C0DEFC",
"G. c #CFE5F5", "A. c #A2BFDD",
"H. c #C3E1FC", "B. c #326299",
"J. c #CAE6FD", "C. c #649DDF",
"K. c #CCEBFD", "D. c #71ABED",
"L. c #C4EBFE", "E. c #3569A4",
"P. c #D6EDFE", "F. c #0D3C71",
"I. c #DAEEFD", "G. c #6998CD",
"U. c #DEF1FE", "H. c #30639D",
"Y. c #D6F2FE", "I. c #A8D3F8",
"T. c #E4F4FE", "J. c #2B5686",
"R. c #E9F6FE", "K. c #3A679B",
"E. c #EBF8FF", "L. c #ADCAEA",
"W. c None", "M. c #85A6C9",
/* pixels */ "N. c #33639B",
"W.W.W.W.W.W.W.W.W.W.W.c.S.L.Y.E.E.S.X.} W.W.W.W.W.W.W.W.W.W.W.W.", "O. c #9CCBFD",
"W.W.W.W.W.W.W.W.W.D.T.E.E.T.L.D.c.z.} } X.} } W.W.W.W.W.W.W.W.W.", "P. c #86C2F7",
"W.W.W.W.W.W.W.B.T.T.R.T.R.U.0.X.z.S.} } } } { { X.W.W.W.W.W.W.W.", "Q. c #0E3C71",
"W.W.W.W.W.W.x.x.K.T.T.T.L.P.q.o.{ } } ` _ { { { { { W.W.W.W.W.W.", "R. c #1B4C83",
"W.W.W.W.W.c.P.D.G.u.r.i 9 Z _ { { G 4 X t { { { { { { W.W.W.W.W.", "S. c #5D95D5",
"W.W.W.W.K.U.n.f O { = t { { { { [ { { W.W.W.W.", "T. c #557BA5",
"W.W.W.F.I.t.. ' t { { [ [ [ [ [ >.W.W.W.", "U. c #85C0F6",
"W.W.x.P.V ' X t ` [ [ [ [ [ [ o.e.W.W.", "V. c #55A8EF",
"W.W.J.y. X t S Y Z $ ' . y [ [ [ ] [ [ | Z.J.W.W.", "W. c #94B3D3",
"W.<.e.& , _ ] ] [ ] U . ' . y [ ' [ ] ] ] w.K.J.g.W.", "X. c #1C497C",
"W.' S o ' ' [ ' [ ' ] o ' . y Y 9 = = 9 @.J.J.J.F.W.", "Y. c #13437A",
"W.| , j ' ' ' ' ' ' ' o ' . $ p A.J.J.g.", "Z. c #487DBB",
"' .. G ' ' ' ' ' ' ' o ' . M H.H.h.", "`. c #7BB7FB",
",.2. . W ' W ' ' ' ' W . ' . M.A.x.", " + c #76B1F5",
"N.M.. . W W W ' W W W W .w 9 I U 0 #.Z.m.", ".+ c #4E85C3",
" .9.O D W W W W ' W j $ % F W W W .5 d Z.C.", "++ c #ACD3FE",
"W W ; 9 9.h.5...Q % o j W W W W W W O. 3 C.N.", "@+ c #2F5989",
"E W 7 B b.d.a . w E E W W W E W E A @ C.l.", "#+ c #7597BC",
"I E l u W E W E W E E E E A . - k.6.", "$+ c #53A7EF",
"P E E 7 m.o E E E E E E E E l . = E P ", "%+ c #C6E1FC",
"L E E E > . O s.o E E E E E E E E 7 , E L ", "&+ c #B6D5F7",
"W.R E R ) #.5 1 6 N i.2 s.+ E E E E E E R L . k R W.", "*+ c #5890D0",
"W.L R E -.m.m.m.m.m.m.2 m.@ N m.m.s.( R R % X E J W.", "=+ c #4076B2",
"W.W.K R ~ a.m.l.l.l.l.2 s.+ < i.l.m.j.h % e K W.W.", "-+ c #619ADB",
"W.W.J R R / l.l.l.l.k.2 s.+ * 5 + 8 R J W.W.", ";+ c #7CB7FC",
"W.W.W.v T R 3.k.k.j.k.2 2 j.& . 8 R v W.W.W.", ">+ c #7DB9FE",
"W.W.W.W.J T ~ 7.j.j.j.g +.p.j.s.+. . . : z T v W.W.W.W.", ",+ c #5087C6",
"W.W.W.W.W.c T T =.f.j.j.s.j.j.j.j.$.g s u e h b T T v W.W.W.W.W.", "'+ c #134479",
"W.W.W.W.W.W.c b n 4.f.f.s.m.s.s.s.j.s.j./ T n T b c W.W.W.W.W.W.", ")+ c #23548D",
"W.W.W.W.W.W.W.c x 1.s.s.s.s.s.s.s.s.4.=.n T n c c W.W.W.W.W.W.W.", "!+ c #24558D",
"W.W.W.W.W.W.W.W.W.&.*.1.a.s.s.s.s.3.n n v x x W.W.W.W.W.W.W.W.W.", "~+ c #8AAACC",
"W.W.W.W.W.W.W.W.W.W.W.%.%.%.%.*.*.Q x x x W.W.W.W.W.W.W.W.W.W.W." "{+ c #A2C1E1",
}; "]+ c #86C1F5",
"^+ c #B4D7FE",
"/+ c #6CA5E8",
"(+ c #22548C",
"_+ c #6D94BF",
":+ c #98B6D6",
"<+ c #134174",
"[+ c #84BDF5",
"}+ c #CAE4FC",
"|+ c #CBE3FD",
"1+ c #8FC3FF",
"2+ c #3F72AD",
"3+ c #49719C",
"4+ c #0C3B70",
"5+ c #9CBBDB",
"6+ c #79B7F3",
"7+ c #BFDCFD",
"8+ c #7FBBFF",
"9+ c #7E9FC3",
"0+ c #77B6F3",
"a+ c #A5CEF7",
"b+ c #9FCBFE",
"c+ c #3267A1",
"d+ c #A4CDF7",
"e+ c #B9D9FA",
"f+ c #C7E1FD",
"g+ c #90C3FF",
"h+ c #15457C",
"i+ c #558CCB",
"j+ c #2E5889",
"k+ c #7B9CC1",
"l+ c #C4DDF6",
"m+ c #BBDAFA",
"n+ c #CDE5FD",
"o+ c #B3D6FE",
"p+ c #80BAFF",
"q+ c #4E84C3",
"r+ c #3E73AF",
"s+ c #78B3F7",
"t+ c #5991D1",
"u+ c #477DBA",
"v+ c #4075B2",
"w+ c #5783B6",
"x+ c #BDD6F0",
"y+ c #A1CBF6",
"z+ c #90C4FF",
"A+ c #BCDBFD",
"B+ c #73B0F1",
"C+ c #C5E0FB",
"D+ c #91C5FF",
"E+ c #AED3FE",
"F+ c #C9E2FC",
"G+ c #76B2F2",
"H+ c #8BBFF9",
"I+ c #81BBFE",
"J+ c #9ECBFE",
"K+ c #84B8F3",
"L+ c #79B4F4",
"M+ c #88BEFA",
"N+ c #83BCFE",
"O+ c #A4CFFC",
"P+ c #A6CDF6",
"Q+ c #82B8F2",
"R+ c #529BEC",
" . + @ # $ % & * = ",
" - ; > > , ' ) ! ~ { { { ] ^ ",
" / ; > > > > ; ( _ : < { { { { { [ } ",
" | 1 2 > > > 2 3 4 5 { { { { { 6 { { { 7 ",
" 8 $ < 9 0 a b c d e { { { { f g h { { { { i ",
" j k l m n n n n n n o { { p q r n s { { { { { i ",
" t u v n n n n n n n n o { { w n n n s { { { { { { x ",
" y z A n n n n n n n n n o { { o n n n s { { { { { { B C ",
" D E n n n F G H I n n n o { { o n n n s { { { { { J K % ",
" L M n n n N O { { s n n n o { { o n n P Q { { { { { R > > S ",
" T n n n n H { { { s n n n o { { o U V 6 W X Y Z ` ...> > +. ",
" @.n n n #.{ { { { s n n n o { { $.%.W &.U n n n n n v *.> > =.",
"-.;.n n n >.{ { { { s n n n ,.{ { { '.).n n n n n n n n !.~.> {.",
"].^.n n n q { { { { s n /.(.{ { _.:.n n n n n n n n n n n m <.[.",
"}.|.n n n H { { { { 1.2.3.{ 4.5.6.n n n 7.8.9.0.a.b.n n n n c.d.",
"e.f.n n n g.{ { { { { { { h.i.n n n n j.k.{ { { l.m.n.n n n o.$ ",
"p.q.n n n /.r.s.t.u.v.w.x.n n n n i.h.{ { { { { { u.o.n n n y.$ ",
"z.A.n n n n B.C.D.u E.F.n n n 6.5.4.{ 3.2.1.{ { { { G.n n n H.d.",
"I.p J.n n n n n n n n n n n K.L.{ { (./.n s { { { { M.n n n N.O.",
"P.{ (.Q.n n n n n n n n R.S.> K _ ,.n n n s { { { { 5.n n n T.U.",
"V.{ { W.X.n n n n n Y.Z.`. +.+> ++o n n n s { { { { @+n n n #+$+",
" %+{ { &+*+Z.=+a -+;+>+,+'+)+> > !+n n n s { { { ~+n n n n {+ ",
" ]+{ { ^+> > > > > /+(+n n )+> > )+n n n _+{ { :+<+n n n o [+ ",
" }+{ |+1+> > > > l n n n )+> > )+n n n 2+~+3+E n n n 4+5+ ",
" 6+{ { 7+8+> > > l n n n )+> > )+n n n n n n n n n F 9+0+ ",
" a+{ { b+> > > l n n n c+> > )+n n n n n n n n r O d+ ",
" e+{ f+g+> > l n h+i+<.> > )+n n n n n E j+k+l+m+ ",
" e+{ n+o+p+q+r+s+> > > > t+u+v+w+2.W.x+{ { e+ ",
" y+{ { z+>+> > > > > > > > > A+{ { { { d+ ",
" B+C+) > > > > > > > > D+E+{ { { F+G+ ",
" H+I+> > > > > > J+{ { { C+K+ ",
" L+M+# N+; 8+O+P+Q+R+ "};

View File

@ -21,5 +21,5 @@ install: doc/qutebrowser.1.html
$(wildcard misc/userscripts/*) $(wildcard misc/userscripts/*)
install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/scripts/" \ install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/scripts/" \
$(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \ $(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \
scripts/testbrowser_cpp scripts/asciidoc2html.py scripts/setupcommon.py \ scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \
scripts/link_pyqt.py,$(wildcard scripts/*)) scripts/link_pyqt.py,$(wildcard scripts/*))

View File

@ -1,5 +1,5 @@
This directory contains various `requirements` files which are used by `tox` to 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. The files are generated based on unpinned requirements in `*.txt-raw` files.

View File

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

View File

@ -1,23 +1,23 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==17.3.0 attrs==17.4.0
flake8==3.5.0 flake8==3.5.0
flake8-bugbear==17.4.0 flake8-bugbear==17.12.0
flake8-builtins==1.0 flake8-builtins==1.0.post0
flake8-comprehensions==1.4.1 flake8-comprehensions==1.4.1
flake8-copyright==0.2.0 flake8-copyright==0.2.0
flake8-debugger==3.0.0 flake8-debugger==3.0.0
flake8-deprecated==1.3 flake8-deprecated==1.3
flake8-docstrings==1.1.0 flake8-docstrings==1.3.0
flake8-future-import==0.4.3 flake8-future-import==0.4.4
flake8-mock==0.3 flake8-mock==0.3
flake8-per-file-ignores==0.4 flake8-per-file-ignores==0.4
flake8-polyfill==1.0.1 flake8-polyfill==1.0.2
flake8-string-format==0.2.3 flake8-string-format==0.2.3
flake8-tidy-imports==1.1.0 flake8-tidy-imports==1.1.0
flake8-tuple==0.2.13 flake8-tuple==0.2.13
mccabe==0.6.1 mccabe==0.6.1
pep8-naming==0.4.1 pep8-naming==0.5.0
pycodestyle==2.3.1 pycodestyle==2.3.1
pydocstyle==2.1.1 pydocstyle==2.1.1
pyflakes==1.6.0 pyflakes==1.6.0

View File

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

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
altgraph==0.14 altgraph==0.15
future==0.16.0 future==0.16.0
macholib==1.8 macholib==1.9
pefile==2017.11.5 pefile==2017.11.5
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller -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 # This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==1.5.3 astroid==1.6.0
certifi==2017.11.5 certifi==2017.11.5
chardet==3.0.4 chardet==3.0.4
github3.py==0.9.6 github3.py==0.9.6
@ -8,7 +8,7 @@ idna==2.6
isort==4.2.15 isort==4.2.15
lazy-object-proxy==1.3.1 lazy-object-proxy==1.3.1
mccabe==0.6.1 mccabe==0.6.1
pylint==1.7.4 pylint==1.8.1
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.18.4 requests==2.18.4
six==1.11.0 six==1.11.0

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.14 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 hg+https://bitbucket.org/pytest-dev/py
git+https://github.com/pytest-dev/pytest.git@features git+https://github.com/pytest-dev/pytest.git@features
git+https://github.com/pytest-dev/pytest-bdd.git 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-cov.git
git+https://github.com/pytest-dev/pytest-faulthandler.git git+https://github.com/pytest-dev/pytest-faulthandler.git
git+https://github.com/pytest-dev/pytest-instafail.git git+https://github.com/pytest-dev/pytest-instafail.git

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==17.3.0 attrs==17.4.0
beautifulsoup4==4.6.0 beautifulsoup4==4.6.0
cheroot==5.10.0 cheroot==6.0.0
click==6.7 click==6.7
# colorama==0.3.9 # colorama==0.3.9
coverage==4.4.2 coverage==4.4.2
@ -11,29 +11,29 @@ fields==5.0.0
Flask==0.12.2 Flask==0.12.2
glob2==0.6 glob2==0.6
hunter==2.0.2 hunter==2.0.2
hypothesis==3.38.5 hypothesis==3.44.16
itsdangerous==0.24 itsdangerous==0.24
# Jinja2==2.9.6 # Jinja2==2.10
Mako==1.0.7 Mako==1.0.7
# MarkupSafe==1.0 # MarkupSafe==1.0
parse==1.8.2 parse==1.8.2
parse-type==0.4.2 parse-type==0.4.2
pluggy==0.6.0
py==1.5.2 py==1.5.2
py-cpuinfo==3.3.0 py-cpuinfo==3.3.0
pytest==3.2.5 pytest==3.3.1 # rq.filter: != 3.3.2
pytest-bdd==2.19.0 pytest-bdd==2.19.0
pytest-benchmark==3.1.1 pytest-benchmark==3.1.1
pytest-catchlog==1.2.2
pytest-cov==2.5.1 pytest-cov==2.5.1
pytest-faulthandler==1.3.1 pytest-faulthandler==1.3.1
pytest-instafail==0.3.0 pytest-instafail==0.3.0
pytest-mock==1.6.3 pytest-mock==1.6.3
pytest-qt==2.3.0 pytest-qt==2.3.1
pytest-repeat==0.4.1 pytest-repeat==0.4.1
pytest-rerunfailures==3.1 pytest-rerunfailures==4.0
pytest-travis-fold==1.2.0 pytest-travis-fold==1.3.0
pytest-xvfb==1.0.0 pytest-xvfb==1.0.0
PyVirtualDisplay==0.2.1 PyVirtualDisplay==0.2.1
six==1.11.0 six==1.11.0
vulture==0.26 vulture==0.26
Werkzeug==0.12.2 Werkzeug==0.14.1

View File

@ -4,10 +4,9 @@ coverage
Flask Flask
hunter hunter
hypothesis hypothesis
pytest pytest==3.3.1
pytest-bdd pytest-bdd
pytest-benchmark pytest-benchmark
pytest-catchlog
pytest-cov pytest-cov
pytest-faulthandler pytest-faulthandler
pytest-instafail pytest-instafail
@ -20,3 +19,4 @@ pytest-xvfb
vulture vulture
#@ ignore: Jinja2, MarkupSafe, colorama #@ ignore: Jinja2, MarkupSafe, colorama
#@ filter: pytest != 3.3.2

View File

@ -2,5 +2,6 @@
pluggy==0.6.0 pluggy==0.6.0
py==1.5.2 py==1.5.2
six==1.11.0
tox==2.9.1 tox==2.9.1
virtualenv==15.1.0 virtualenv==15.1.0

View File

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

View File

@ -41,7 +41,7 @@
[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com' [ -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 "$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 [ -z "${url// }" ] && exit

View File

@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
set -euo pipefail
# #
# Behavior: # Behavior:
# Userscript for qutebrowser which will take the raw JSON text of the current # Userscript for qutebrowser which will take the raw JSON text of the current
@ -19,29 +20,23 @@
# #
# Bryan Gilbert, 2017 # 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 # default style to monokai if none is provided
STYLE=${1:-monokai} 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 TEMP_FILE="$(mktemp)"
if [ $? -ne 0 ] || [ -z "$FORMATTED_JSON" ]; then jq . "$QUTE_TEXT" >"$TEMP_FILE"
echo "Invalid json, aborting..."
exit 1 # 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 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 # send the command to qutebrowser to open the new file containing the formatted json
echo "open -t file://$TEMP_FILE" >> "$QUTE_FIFO" echo "open -t file://$TEMP_FILE" >> "$QUTE_FIFO"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ msg() {
MPV_COMMAND=${MPV_COMMAND:-mpv} MPV_COMMAND=${MPV_COMMAND:-mpv}
# Warning: spaces in single flags are not supported # 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=} 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() { js() {
cat <<EOF cat <<EOF

View File

@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2017 Florian Bruhin (The Compiler)"
__license__ = "GPL" __license__ = "GPL"
__maintainer__ = __author__ __maintainer__ = __author__
__email__ = "mail@qutebrowser.org" __email__ = "mail@qutebrowser.org"
__version_info__ = (1, 0, 4) __version_info__ = (1, 1, 0)
__version__ = '.'.join(str(e) for e in __version_info__) __version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5." __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.commands import cmdutils, runners, cmdexc
from qutebrowser.config import config, websettings, configfiles, configinit from qutebrowser.config import config, websettings, configfiles, configinit
from qutebrowser.browser import (urlmarks, adblock, history, browsertab, from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads) downloads, greasemonkey)
from qutebrowser.browser.network import proxy from qutebrowser.browser.network import proxy
from qutebrowser.browser.webkit import cookies, cache from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit.network import networkmanager
@ -491,6 +491,9 @@ def _init_modules(args, crash_handler):
diskcache = cache.DiskCache(standarddir.cache(), parent=qApp) diskcache = cache.DiskCache(standarddir.cache(), parent=qApp)
objreg.register('cache', diskcache) objreg.register('cache', diskcache)
log.init.debug("Initializing Greasemonkey...")
greasemonkey.init()
log.init.debug("Misc initialization...") log.init.debug("Misc initialization...")
macros.init() macros.init()
# Init backend-specific stuff # Init backend-specific stuff
@ -561,8 +564,8 @@ class Quitter:
cwd = os.path.abspath(os.path.dirname(sys.executable)) cwd = os.path.abspath(os.path.dirname(sys.executable))
else: else:
args = [sys.executable, '-m', 'qutebrowser'] args = [sys.executable, '-m', 'qutebrowser']
cwd = os.path.join(os.path.abspath(os.path.dirname( cwd = os.path.join(
qutebrowser.__file__)), '..') os.path.abspath(os.path.dirname(qutebrowser.__file__)), '..')
if not os.path.isdir(cwd): if not os.path.isdir(cwd):
# Probably running from a python egg. Let's fallback to # Probably running from a python egg. Let's fallback to
# cwd=None and see if that works out. # cwd=None and see if that works out.
@ -869,10 +872,6 @@ class EventFilter(QObject):
super().__init__(parent) super().__init__(parent)
self._activated = True self._activated = True
self._handlers = { self._handlers = {
QEvent.MouseButtonDblClick: self._handle_mouse_event,
QEvent.MouseButtonPress: self._handle_mouse_event,
QEvent.MouseButtonRelease: self._handle_mouse_event,
QEvent.MouseMove: self._handle_mouse_event,
QEvent.KeyPress: self._handle_key_event, QEvent.KeyPress: self._handle_key_event,
QEvent.KeyRelease: self._handle_key_event, QEvent.KeyRelease: self._handle_key_event,
} }
@ -897,19 +896,6 @@ class EventFilter(QObject):
# No window available yet, or not a MainWindow # No window available yet, or not a MainWindow
return False return False
def _handle_mouse_event(self, _event):
"""Handle a mouse event.
Args:
_event: The QEvent which is about to be delivered.
Return:
True if the event should be filtered, False if it's passed through.
"""
# Mouse cursor shown (overrideCursor None) -> don't filter event
# Mouse cursor hidden (overrideCursor not None) -> filter event
return qApp.overrideCursor() is not None
def eventFilter(self, obj, event): def eventFilter(self, obj, event):
"""Handle an event. """Handle an event.

View File

@ -487,6 +487,7 @@ class AbstractHistory:
raise NotImplementedError raise NotImplementedError
def back(self, count=1): def back(self, count=1):
"""Go back in the tab's history."""
idx = self.current_idx() - count idx = self.current_idx() - count
if idx >= 0: if idx >= 0:
self._go_to_item(self._item_at(idx)) self._go_to_item(self._item_at(idx))
@ -495,6 +496,7 @@ class AbstractHistory:
raise WebTabError("At beginning of history.") raise WebTabError("At beginning of history.")
def forward(self, count=1): def forward(self, count=1):
"""Go forward in the tab's history."""
idx = self.current_idx() + count idx = self.current_idx() + count
if idx < len(self): if idx < len(self):
self._go_to_item(self._item_at(idx)) self._go_to_item(self._item_at(idx))
@ -704,7 +706,7 @@ class AbstractTab(QWidget):
# This only gives us some mild protection against re-using events, but # This only gives us some mild protection against re-using events, but
# it's certainly better than a segfault. # it's certainly better than a segfault.
if getattr(evt, 'posted', False): if getattr(evt, 'posted', False):
raise AssertionError("Can't re-use an event which was already " raise utils.Unreachable("Can't re-use an event which was already "
"posted!") "posted!")
recipient = self.event_target() recipient = self.event_target()
evt.posted = True evt.posted = True

View File

@ -39,7 +39,7 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem, downloads) webelem, downloads)
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, debug, standarddir) objreg, utils, standarddir)
from qutebrowser.utils.usertypes import KeyMode from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess from qutebrowser.misc import editor, guiprocess
from qutebrowser.completion.models import urlmodel, miscmodels from qutebrowser.completion.models import urlmodel, miscmodels
@ -518,7 +518,7 @@ class CommandDispatcher:
return newtab return newtab
@cmdutils.register(instance='command-dispatcher', scope='window') @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): def tab_take(self, index):
"""Take a tab from another window. """Take a tab from another window.
@ -953,22 +953,25 @@ class CommandDispatcher:
(prev and i < cur_idx) or (prev and i < cur_idx) or
(next_ and i > cur_idx)) (next_ and i > cur_idx))
# Check to see if we are closing any pinned tabs # close as many tabs as we can
if not force:
for i, tab in enumerate(self._tabbed_browser.widgets()):
if _to_close(i) and tab.data.pinned:
self._tabbed_browser.tab_close_prompt_if_pinned(
tab,
force,
lambda: self.tab_only(
prev=prev, next_=next_, force=True))
return
first_tab = True first_tab = True
pinned_tabs_cleanup = False
for i, tab in enumerate(self._tabbed_browser.widgets()): for i, tab in enumerate(self._tabbed_browser.widgets()):
if _to_close(i): if _to_close(i):
if force or not tab.data.pinned:
self._tabbed_browser.close_tab(tab, new_undo=first_tab) self._tabbed_browser.close_tab(tab, new_undo=first_tab)
first_tab = False first_tab = False
else:
pinned_tabs_cleanup = tab
# Check to see if we would like to close any pinned tabs
if pinned_tabs_cleanup:
self._tabbed_browser.tab_close_prompt_if_pinned(
pinned_tabs_cleanup,
force,
lambda: self.tab_only(
prev=prev, next_=next_, force=True),
text="Are you sure you want to close pinned tabs?")
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def undo(self): def undo(self):
@ -1177,7 +1180,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_replace_variables=True) 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. """Spawn a command in a shell.
Args: Args:
@ -1188,6 +1192,7 @@ class CommandDispatcher:
(or `$XDG_DATA_DIR`) (or `$XDG_DATA_DIR`)
- `/usr/share/qutebrowser/userscripts` - `/usr/share/qutebrowser/userscripts`
verbose: Show notifications when the command started/exited. 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. detach: Whether the command should be detached from qutebrowser.
cmdline: The commandline to execute. cmdline: The commandline to execute.
""" """
@ -1214,6 +1219,11 @@ class CommandDispatcher:
else: else:
proc.start(cmd, args) 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') @cmdutils.register(instance='command-dispatcher', scope='window')
def home(self): def home(self):
"""Open main startpage in current tab.""" """Open main startpage in current tab."""
@ -1418,27 +1428,14 @@ class CommandDispatcher:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('dest_old', hide=True) def download(self, url=None, *, mhtml_=False, dest=None):
def download(self, url=None, dest_old=None, *, mhtml_=False, dest=None):
"""Download a given URL, or current page if no URL given. """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: Args:
url: The URL to download. If not given, download the current page. 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. dest: The file path to write the download to, or None to ask.
mhtml_: Download the current page and all assets as mhtml file. 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? # FIXME:qtwebengine do this with the QtWebEngine download manager?
download_manager = objreg.get('qtnetwork-download-manager', download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window=self._win_id) scope='window', window=self._win_id)
@ -1528,6 +1525,7 @@ class CommandDispatcher:
dest = os.path.expanduser(dest) dest = os.path.expanduser(dest)
def callback(data): def callback(data):
"""Write the data to disk."""
try: try:
with open(dest, 'w', encoding='utf-8') as f: with open(dest, 'w', encoding='utf-8') as f:
f.write(data) f.write(data)
@ -1649,6 +1647,8 @@ class CommandDispatcher:
except webelem.Error as e: except webelem.Error as e:
raise cmdexc.CommandError(str(e)) raise cmdexc.CommandError(str(e))
mainwindow.raise_window(objreg.last_focused_window(), alert=False)
@cmdutils.register(instance='command-dispatcher', maxsplit=0, @cmdutils.register(instance='command-dispatcher', maxsplit=0,
scope='window') scope='window')
def insert_text(self, text): def insert_text(self, text):
@ -1742,7 +1742,8 @@ class CommandDispatcher:
elif going_up and tab.scroller.pos_px().y() > old_scroll_pos.y(): elif going_up and tab.scroller.pos_px().y() > old_scroll_pos.y():
message.info("Search hit TOP, continuing at BOTTOM") message.info("Search hit TOP, continuing at BOTTOM")
else: 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', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0) maxsplit=0)
@ -1762,7 +1763,7 @@ class CommandDispatcher:
return return
options = { options = {
'ignore_case': config.val.ignore_case, 'ignore_case': config.val.search.ignore_case,
'reverse': reverse, 'reverse': reverse,
} }
@ -2045,6 +2046,7 @@ class CommandDispatcher:
jseval_cb = None jseval_cb = None
else: else:
def jseval_cb(out): def jseval_cb(out):
"""Show the data returned from JS."""
if out is None: if out is None:
# Getting the actual error (if any) seems to be difficult. # Getting the actual error (if any) seems to be difficult.
# The error does end up in # The error does end up in
@ -2197,11 +2199,4 @@ class CommandDispatcher:
return return
window = self._tabbed_browser.window() window = self._tabbed_browser.window()
if window.isFullScreen(): window.setWindowState(window.windowState() ^ Qt.WindowFullScreen)
window.setWindowState(
window.state_before_fullscreen & ~Qt.WindowFullScreen)
else:
window.state_before_fullscreen = window.windowState()
window.showFullScreen()
log.misc.debug('state before fullscreen: {}'.format(
debug.qflags_key(Qt, window.state_before_fullscreen)))

View File

@ -103,6 +103,8 @@ def immediate_download_path(prompt_download_directory=None):
if not prompt_download_directory: if not prompt_download_directory:
return download_dir() return download_dir()
return None
def _path_suggestion(filename): def _path_suggestion(filename):
"""Get the suggested file path. """Get the suggested file path.
@ -180,7 +182,7 @@ def transform_path(path):
path = utils.expand_windows_drive(path) path = utils.expand_windows_drive(path)
# Drive dependent working directories are not supported, e.g. # Drive dependent working directories are not supported, e.g.
# E:filename is invalid # E:filename is invalid
if re.match(r'[A-Z]:[^\\]', path, re.IGNORECASE): if re.search(r'^[A-Z]:[^\\]', path, re.IGNORECASE):
return None return None
# Paths like COM1, ... # Paths like COM1, ...
# See https://github.com/qutebrowser/qutebrowser/issues/82 # See https://github.com/qutebrowser/qutebrowser/issues/82

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): def _cleanup(self):
"""Clean up after hinting.""" """Clean up after hinting."""
# pylint: disable=not-an-iterable
for label in self._context.all_labels: for label in self._context.all_labels:
label.cleanup() label.cleanup()
# pylint: enable=not-an-iterable
text = self._get_text() text = self._get_text()
message_bridge = objreg.get('message-bridge', scope='window', message_bridge = objreg.get('message-bridge', scope='window',
@ -621,8 +619,9 @@ class HintManager(QObject):
@cmdutils.register(instance='hintmanager', scope='tab', name='hint', @cmdutils.register(instance='hintmanager', scope='tab', name='hint',
star_args_optional=True, maxsplit=2) star_args_optional=True, maxsplit=2)
@cmdutils.argument('win_id', win_id=True) @cmdutils.argument('win_id', win_id=True)
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal, def start(self, # pylint: disable=keyword-arg-before-vararg
*args, win_id, mode=None, add_history=False): group=webelem.Group.all, target=Target.normal,
*args, win_id, mode=None, add_history=False, rapid=False):
"""Start hinting. """Start hinting.
Args: Args:
@ -809,7 +808,6 @@ class HintManager(QObject):
log.hints.debug("Filtering hints on {!r}".format(filterstr)) log.hints.debug("Filtering hints on {!r}".format(filterstr))
visible = [] visible = []
# pylint: disable=not-an-iterable
for label in self._context.all_labels: for label in self._context.all_labels:
try: try:
if self._filter_matches(filterstr, str(label.elem)): if self._filter_matches(filterstr, str(label.elem)):
@ -821,7 +819,6 @@ class HintManager(QObject):
label.hide() label.hide()
except webelem.Error: except webelem.Error:
pass pass
# pylint: enable=not-an-iterable
if not visible: if not visible:
# Whoops, filtered all hints # Whoops, filtered all hints

View File

@ -32,7 +32,7 @@ from qutebrowser.misc import objects, sql
# increment to indicate that HistoryCompletion must be regenerated # increment to indicate that HistoryCompletion must be regenerated
_USER_VERSION = 1 _USER_VERSION = 2
class CompletionHistory(sql.SqlTable): class CompletionHistory(sql.SqlTable):
@ -102,7 +102,8 @@ class WebHistory(sql.SqlTable):
data = {'url': [], 'title': [], 'last_atime': []} data = {'url': [], 'title': [], 'last_atime': []}
# select the latest entry for each url # select the latest entry for each url
q = sql.Query('SELECT url, title, max(atime) AS atime FROM History ' q = sql.Query('SELECT url, title, max(atime) AS atime FROM History '
'WHERE NOT redirect GROUP BY url ORDER BY atime asc') 'WHERE NOT redirect and url NOT LIKE "qute://back%" '
'GROUP BY url ORDER BY atime asc')
for entry in q.run(): for entry in q.run():
data['url'].append(self._format_completion_url(QUrl(entry.url))) data['url'].append(self._format_completion_url(QUrl(entry.url)))
data['title'].append(entry.title) data['title'].append(entry.title)
@ -149,8 +150,8 @@ class WebHistory(sql.SqlTable):
if force: if force:
self._do_clear() self._do_clear()
else: else:
message.confirm_async(self._do_clear, title="Clear all browsing " message.confirm_async(yes_action=self._do_clear,
"history?") title="Clear all browsing history?")
def _do_clear(self): def _do_clear(self):
with self._handle_sql_errors(): with self._handle_sql_errors():
@ -171,7 +172,9 @@ class WebHistory(sql.SqlTable):
@pyqtSlot(QUrl, QUrl, str) @pyqtSlot(QUrl, QUrl, str)
def add_from_tab(self, url, requested_url, title): def add_from_tab(self, url, requested_url, title):
"""Add a new history entry as slot, called from a BrowserTab.""" """Add a new history entry as slot, called from a BrowserTab."""
if url.scheme() == 'data' or requested_url.scheme() == 'data': if any(url.scheme() == 'data' or
(url.scheme(), url.host()) == ('qute', 'back')
for url in (url, requested_url)):
return return
if url.isEmpty(): if url.isEmpty():
# things set via setHtml # things set via setHtml
@ -268,6 +271,7 @@ class WebHistory(sql.SqlTable):
return return
def action(): def action():
"""Actually run the import."""
with debug.log_time(log.init, 'Import old history file to sqlite'): with debug.log_time(log.init, 'Import old history file to sqlite'):
try: try:
self._read(path) self._read(path)
@ -340,7 +344,7 @@ class WebHistory(sql.SqlTable):
f.write('\n'.join(lines)) f.write('\n'.join(lines))
message.info("Dumped history to {}".format(dest)) message.info("Dumped history to {}".format(dest))
except OSError as e: 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): def init(parent=None):

View File

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

View File

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

View File

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

View File

@ -303,7 +303,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
"""Handle QNetworkReply errors.""" """Handle QNetworkReply errors."""
if code == QNetworkReply.OperationCanceledError: if code == QNetworkReply.OperationCanceledError:
return return
else:
self._die(self._reply.errorString()) self._die(self._reply.errorString())
@pyqtSlot() @pyqtSlot()
@ -399,7 +398,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
""" """
if not url.isValid(): if not url.isValid():
urlutils.invalid_url_error(url, "start download") urlutils.invalid_url_error(url, "start download")
return return None
req = QNetworkRequest(url) req = QNetworkRequest(url)
if user_agent is not None: if user_agent is not None:
req.setHeader(QNetworkRequest.UserAgentHeader, user_agent) req.setHeader(QNetworkRequest.UserAgentHeader, user_agent)

View File

@ -29,6 +29,7 @@ import os
import time import time
import textwrap import textwrap
import mimetypes import mimetypes
import urllib
import pkg_resources import pkg_resources
from PyQt5.QtCore import QUrlQuery, QUrl from PyQt5.QtCore import QUrlQuery, QUrl
@ -41,6 +42,7 @@ from qutebrowser.misc import objects
pyeval_output = ":pyeval was never called" pyeval_output = ":pyeval was never called"
spawn_output = ":spawn was never called"
_HANDLERS = {} _HANDLERS = {}
@ -111,6 +113,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
return function return function
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
"""Call the underlying function."""
if self._backend is not None and objects.backend != self._backend: if self._backend is not None and objects.backend != self._backend:
return self.wrong_backend_handler(*args, **kwargs) return self.wrong_backend_handler(*args, **kwargs)
else: else:
@ -267,6 +270,13 @@ def qute_pyeval(_url):
return 'text/html', html 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('version')
@add_handler('verizon') @add_handler('verizon')
def qute_version(_url): def qute_version(_url):
@ -425,6 +435,18 @@ def qute_settings(url):
return 'text/html', html 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') @add_handler('configdiff')
def qute_configdiff(url): def qute_configdiff(url):
"""Handler for qute://configdiff.""" """Handler for qute://configdiff."""

View File

@ -22,7 +22,7 @@
import html import html
from qutebrowser.config import config 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 from qutebrowser.mainwindow import mainwindow
@ -182,7 +182,7 @@ def ignore_certificate_errors(url, errors, abort_on):
return False return False
else: else:
raise ValueError("Invalid ssl_strict value {!r}".format(ssl_strict)) 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): 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 os
import html
import os.path import os.path
import html
import functools import functools
import collections import collections

View File

@ -42,7 +42,9 @@ Group = enum.Enum('Group', ['all', 'links', 'images', 'url', 'inputs'])
SELECTORS = { SELECTORS = {
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, ' Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
'frame, iframe, link, [onclick], [onmousedown], [role=link], ' 'frame, iframe, link, [onclick], [onmousedown], [role=link], '
'[role=option], [role=button], img'), '[role=option], [role=button], img, '
# Angular 1 selectors
'[ng-click], [ngClick], [data-ng-click], [x-ng-click]'),
Group.links: 'a[href], area[href], link[href], [role=link][href]', Group.links: 'a[href], area[href], link[href], [role=link][href]',
Group.images: 'img', Group.images: 'img',
Group.url: '[src], [href]', Group.url: '[src], [href]',
@ -60,7 +62,7 @@ class Error(Exception):
pass pass
class OrphanedError(Exception): class OrphanedError(Error):
"""Raised when a webelement's parent has vanished.""" """Raised when a webelement's parent has vanished."""

View File

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

View File

@ -211,11 +211,11 @@ class WebEngineElement(webelem.AbstractWebElement):
def _click_js(self, _click_target): def _click_js(self, _click_target):
# FIXME:qtwebengine Have a proper API for this # FIXME:qtwebengine Have a proper API for this
# pylint: disable=protected-access # pylint: disable=protected-access
settings = self._tab._widget.settings() view = self._tab._widget
# pylint: enable=protected-access # pylint: enable=protected-access
attribute = QWebEngineSettings.JavascriptCanOpenWindows attribute = QWebEngineSettings.JavascriptCanOpenWindows
could_open_windows = settings.testAttribute(attribute) could_open_windows = view.settings().testAttribute(attribute)
settings.setAttribute(attribute, True) view.settings().setAttribute(attribute, True)
# Get QtWebEngine do apply the settings # Get QtWebEngine do apply the settings
# (it does so with a 0ms QTimer...) # (it does so with a 0ms QTimer...)
@ -226,6 +226,12 @@ class WebEngineElement(webelem.AbstractWebElement):
QEventLoop.ExcludeUserInputEvents) QEventLoop.ExcludeUserInputEvents)
def reset_setting(_arg): 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) self._js_call('click', callback=reset_setting)

View File

@ -244,6 +244,43 @@ def _init_profiles():
private_profile.setSpellCheckEnabled(True) 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): def init(args):
"""Initialize the global QWebSettings.""" """Initialize the global QWebSettings."""
if args.enable_webengine_inspector: if args.enable_webengine_inspector:

View File

@ -24,7 +24,8 @@ import functools
import html as html_utils import html as html_utils
import sip 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.QtGui import QKeyEvent
from PyQt5.QtNetwork import QAuthenticator from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
@ -69,6 +70,10 @@ def init():
download_manager.install(webenginesettings.private_profile) download_manager.install(webenginesettings.private_profile)
objreg.register('webengine-download-manager', download_manager) 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. # Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
_JS_WORLD_MAP = { _JS_WORLD_MAP = {
@ -121,18 +126,35 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
class WebEngineSearch(browsertab.AbstractSearch): 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): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._flags = QWebEnginePage.FindFlags(0) self._flags = QWebEnginePage.FindFlags(0)
self._pending_searches = 0
def _find(self, text, flags, callback, caller): def _find(self, text, flags, callback, caller):
"""Call findText on the widget.""" """Call findText on the widget."""
self.search_displayed = True self.search_displayed = True
self._pending_searches += 1
def wrapped_callback(found): def wrapped_callback(found):
"""Wrap the callback to do debug logging.""" """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" found_text = 'found' if found else "didn't find"
if flags: if flags:
flag_text = 'with flags {}'.format(debug.qflags_key( flag_text = 'with flags {}'.format(debug.qflags_key(
@ -518,7 +540,15 @@ class WebEngineElements(browsertab.AbstractElements):
class WebEngineTab(browsertab.AbstractTab): 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): def __init__(self, *, win_id, mode_manager, private, parent=None):
super().__init__(win_id=win_id, mode_manager=mode_manager, super().__init__(win_id=win_id, mode_manager=mode_manager,
@ -772,6 +802,24 @@ class WebEngineTab(browsertab.AbstractTab):
} }
self.renderer_process_terminated.emit(status_map[status], exitcode) 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): def _connect_signals(self):
view = self._widget view = self._widget
page = view.page() page = view.page()
@ -780,9 +828,6 @@ class WebEngineTab(browsertab.AbstractTab):
page.linkHovered.connect(self.link_hovered) page.linkHovered.connect(self.link_hovered)
page.loadProgress.connect(self._on_load_progress) page.loadProgress.connect(self._on_load_progress)
page.loadStarted.connect(self._on_load_started) 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.certificate_error.connect(self._on_ssl_errors)
page.authenticationRequired.connect(self._on_authentication_required) page.authenticationRequired.connect(self._on_authentication_required)
page.proxyAuthenticationRequired.connect( page.proxyAuthenticationRequired.connect(
@ -795,6 +840,19 @@ class WebEngineTab(browsertab.AbstractTab):
view.renderProcessTerminated.connect( view.renderProcessTerminated.connect(
self._on_render_process_terminated) self._on_render_process_terminated)
view.iconChanged.connect(self.icon_changed) 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): def event_target(self):
return self._widget.focusProxy() return self._widget.focusProxy()

View File

@ -23,12 +23,14 @@ import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
from PyQt5.QtGui import QPalette 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 import shared
from qutebrowser.browser.webengine import certificateerror, webenginesettings from qutebrowser.browser.webengine import certificateerror, webenginesettings
from qutebrowser.config import config 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): class WebEngineView(QWebEngineView):
@ -135,6 +137,7 @@ class WebEnginePage(QWebEnginePage):
self._theme_color = theme_color self._theme_color = theme_color
self._set_bg_color() self._set_bg_color()
config.instance.changed.connect(self._set_bg_color) config.instance.changed.connect(self._set_bg_color)
self.urlChanged.connect(self._inject_userjs)
@config.change_filter('colors.webpage.bg') @config.change_filter('colors.webpage.bg')
def _set_bg_color(self): def _set_bg_color(self):
@ -300,3 +303,43 @@ class WebEnginePage(QWebEnginePage):
message.error(msg) message.error(msg)
return False return False
return True 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 import os.path
from PyQt5.QtNetwork import QNetworkRequest
from qutebrowser.utils import log from qutebrowser.utils import log
from qutebrowser.browser.webkit import rfc6266 from qutebrowser.browser.webkit import rfc6266
from PyQt5.QtNetwork import QNetworkRequest
def parse_content_disposition(reply): def parse_content_disposition(reply):
"""Parse a content_disposition header. """Parse a content_disposition header.
@ -57,9 +57,7 @@ def parse_content_disposition(reply):
is_inline = content_disposition.is_inline() is_inline = content_disposition.is_inline()
# Then try to get filename from url # Then try to get filename from url
if not filename: if not filename:
path = reply.url().path() filename = reply.url().path().rstrip('/')
if path is not None:
filename = path.rstrip('/')
# If that fails as well, use a fallback # If that fails as well, use a fallback
if not filename: if not filename:
filename = 'qutebrowser-download' filename = 'qutebrowser-download'

View File

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

View File

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

View File

@ -19,6 +19,7 @@
"""Wrapper over our (QtWebKit) WebView.""" """Wrapper over our (QtWebKit) WebView."""
import re
import functools import functools
import xml.etree.ElementTree import xml.etree.ElementTree
@ -541,10 +542,15 @@ class WebKitElements(browsertab.AbstractElements):
def find_id(self, elem_id, callback): def find_id(self, elem_id, callback):
def find_id_cb(elems): def find_id_cb(elems):
"""Call the real callback with the found elements."""
if not elems: if not elems:
callback(None) callback(None)
else: else:
callback(elems[0]) 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) self.find_css('#' + elem_id, find_id_cb)
def find_focused(self, callback): def find_focused(self, callback):

View File

@ -86,6 +86,21 @@ class BrowserPage(QWebPage):
self.on_save_frame_state_requested) self.on_save_frame_state_requested)
self.restoreFrameStateRequested.connect( self.restoreFrameStateRequested.connect(
self.on_restore_frame_state_requested) 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): def javaScriptPrompt(self, frame, js_msg, default):
"""Override javaScriptPrompt to use qutebrowser prompts.""" """Override javaScriptPrompt to use qutebrowser prompts."""
@ -283,6 +298,38 @@ class BrowserPage(QWebPage):
else: else:
self.error_occurred = False 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') @pyqtSlot('QWebFrame*', 'QWebPage::Feature')
def _on_feature_permission_requested(self, frame, feature): def _on_feature_permission_requested(self, frame, feature):
"""Ask the user for approval for geolocation/notifications.""" """Ask the user for approval for geolocation/notifications."""

View File

@ -190,6 +190,7 @@ class Command:
return True return True
elif arg_info.win_id: elif arg_info.win_id:
return True return True
return False
def _inspect_func(self): def _inspect_func(self):
"""Inspect the function to get useful informations from it. """Inspect the function to get useful informations from it.

View File

@ -126,7 +126,7 @@ class CommandParser:
new_cmd += ' ' new_cmd += ' '
return 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. """Split a command on ;; and parse all parts.
If the first command in the commandline is a non-split one, it only 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() config = attr.ib()
keyconf = attr.ib() keyconf = attr.ib()
win_id = attr.ib()
class Completer(QObject): class Completer(QObject):
@ -43,6 +44,7 @@ class Completer(QObject):
Attributes: Attributes:
_cmd: The statusbar Command object this completer belongs to. _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. _timer: The timer used to trigger the completion update.
_last_cursor_pos: The old cursor position so we avoid double completion _last_cursor_pos: The old cursor position so we avoid double completion
updates. updates.
@ -50,9 +52,10 @@ class Completer(QObject):
_last_completion_func: The completion function used for the last text. _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) super().__init__(parent)
self._cmd = cmd self._cmd = cmd
self._win_id = win_id
self._timer = QTimer() self._timer = QTimer()
self._timer.setSingleShot(True) self._timer.setSingleShot(True)
self._timer.setInterval(0) self._timer.setInterval(0)
@ -84,8 +87,6 @@ class Completer(QObject):
# cursor on a flag or after an explicit split (--) # cursor on a flag or after an explicit split (--)
return None return None
log.completion.debug("Before removing flags: {}".format(before_cursor)) log.completion.debug("Before removing flags: {}".format(before_cursor))
before_cursor = [x for x in before_cursor if not x.startswith('-')]
log.completion.debug("After removing flags: {}".format(before_cursor))
if not before_cursor: if not before_cursor:
# '|' or 'set|' # '|' or 'set|'
log.completion.debug('Starting command completion') log.completion.debug('Starting command completion')
@ -96,6 +97,9 @@ class Completer(QObject):
log.completion.debug("No completion for unknown command: {}" log.completion.debug("No completion for unknown command: {}"
.format(before_cursor[0])) .format(before_cursor[0]))
return None return None
before_cursor = [x for x in before_cursor if not x.startswith('-')]
log.completion.debug("After removing flags: {}".format(before_cursor))
argpos = len(before_cursor) - 1 argpos = len(before_cursor) - 1
try: try:
func = cmd.get_pos_arg_info(argpos).completion func = cmd.get_pos_arg_info(argpos).completion
@ -131,9 +135,7 @@ class Completer(QObject):
return [], '', [] return [], '', []
parser = runners.CommandParser() parser = runners.CommandParser()
result = parser.parse(text, fallback=True, keep=True) result = parser.parse(text, fallback=True, keep=True)
# pylint: disable=not-an-iterable
parts = [x for x in result.cmdline if x] parts = [x for x in result.cmdline if x]
# pylint: enable=not-an-iterable
pos = self._cmd.cursorPosition() - len(self._cmd.prefix()) pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars
log.completion.debug('partitioning {} around position {}'.format(parts, log.completion.debug('partitioning {} around position {}'.format(parts,
@ -152,8 +154,7 @@ class Completer(QObject):
"partitioned: {} '{}' {}".format(prefix, center, postfix)) "partitioned: {} '{}' {}".format(prefix, center, postfix))
return prefix, center, postfix return prefix, center, postfix
# We should always return above raise utils.Unreachable("Not all parts consumed: {}".format(parts))
assert False, parts
@pyqtSlot(str) @pyqtSlot(str)
def on_selection_changed(self, text): def on_selection_changed(self, text):
@ -247,10 +248,11 @@ class Completer(QObject):
if func != self._last_completion_func: if func != self._last_completion_func:
self._last_completion_func = func self._last_completion_func = func
args = (x for x in before_cursor[1:] if not x.startswith('-')) args = (x for x in before_cursor[1:] if not x.startswith('-'))
with debug.log_time(log.completion, with debug.log_time(log.completion, 'Starting {} completion'
'Starting {} completion'.format(func.__name__)): .format(func.__name__)):
info = CompletionInfo(config=config.instance, info = CompletionInfo(config=config.instance,
keyconf=config.key_instance) keyconf=config.key_instance,
win_id=self._win_id)
model = func(*args, info=info) model = func(*args, info=info)
with debug.log_time(log.completion, 'Set completion model'): with debug.log_time(log.completion, 'Set completion model'):
completion.set_model(model) completion.set_model(model)

View File

@ -138,10 +138,10 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._painter.translate(text_rect.left(), text_rect.top()) self._painter.translate(text_rect.left(), text_rect.top())
self._get_textdoc(index) self._get_textdoc(index)
self._draw_textdoc(text_rect) self._draw_textdoc(text_rect, index.column())
self._painter.restore() self._painter.restore()
def _draw_textdoc(self, rect): def _draw_textdoc(self, rect, col):
"""Draw the QTextDocument of an item. """Draw the QTextDocument of an item.
Args: Args:
@ -156,7 +156,9 @@ class CompletionItemDelegate(QStyledItemDelegate):
elif not self._opt.state & QStyle.State_Enabled: elif not self._opt.state & QStyle.State_Enabled:
color = config.val.colors.completion.category.fg color = config.val.colors.completion.category.fg
else: 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) self._painter.setPen(color)
ctx = QAbstractTextDocumentLayout.PaintContext() ctx = QAbstractTextDocumentLayout.PaintContext()

View File

@ -23,7 +23,7 @@ Defines a CompletionView which uses CompletionFiterModel and CompletionModel
subclasses to provide completions. 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 PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config from qutebrowser.config import config
@ -152,12 +152,12 @@ class CompletionView(QTreeView):
column_widths = self.model().column_widths column_widths = self.model().column_widths
pixel_widths = [(width * perc // 100) for perc in column_widths] pixel_widths = [(width * perc // 100) for perc in column_widths]
if self.verticalScrollBar().isVisible(): delta = self.verticalScrollBar().sizeHint().width()
delta = self.style().pixelMetric(QStyle.PM_ScrollBarExtent) + 5
if pixel_widths[-1] > delta: if pixel_widths[-1] > delta:
pixel_widths[-1] -= delta pixel_widths[-1] -= delta
else: else:
pixel_widths[-2] -= delta pixel_widths[-2] -= delta
for i, w in enumerate(pixel_widths): for i, w in enumerate(pixel_widths):
assert w >= 0, i assert w >= 0, i
self.setColumnWidth(i, w) self.setColumnWidth(i, w)
@ -180,6 +180,7 @@ class CompletionView(QTreeView):
return self.model().last_item() return self.model().last_item()
else: else:
return self.model().first_item() return self.model().first_item()
while True: while True:
idx = self.indexAbove(idx) if upwards else self.indexBelow(idx) idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)
# wrap around if we arrived at beginning/end # 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 # Item is a real item, not a category header -> success
return idx return idx
raise utils.Unreachable
def _next_category_idx(self, upwards): def _next_category_idx(self, upwards):
"""Get the index of the previous/next category. """Get the index of the previous/next category.
@ -222,6 +225,8 @@ class CompletionView(QTreeView):
self.scrollTo(idx) self.scrollTo(idx)
return idx.child(0, 0) return idx.child(0, 0)
raise utils.Unreachable
@cmdutils.register(instance='completion', @cmdutils.register(instance='completion',
modes=[usertypes.KeyMode.command], scope='window') modes=[usertypes.KeyMode.command], scope='window')
@cmdutils.argument('which', choices=['next', 'prev', 'next-category', @cmdutils.argument('which', choices=['next', 'prev', 'next-category',

View File

@ -60,7 +60,8 @@ def value(optname, *_values, info):
opt = info.config.get_opt(optname) opt = info.config.get_opt(optname)
default = opt.typ.to_str(opt.default) 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")]) [(current, "Current value"), (default, "Default value")])
model.add_category(cur_cat) model.add_category(cur_cat)

View File

@ -19,8 +19,6 @@
"""A completion category that queries the SQL History store.""" """A completion category that queries the SQL History store."""
import re
from PyQt5.QtSql import QSqlQueryModel from PyQt5.QtSql import QSqlQueryModel
from qutebrowser.misc import sql from qutebrowser.misc import sql
@ -36,21 +34,7 @@ class HistoryCategory(QSqlQueryModel):
"""Create a new History completion category.""" """Create a new History completion category."""
super().__init__(parent=parent) super().__init__(parent=parent)
self.name = "History" self.name = "History"
self._query = None
# 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)
# advertise that this model filters by URL and title # advertise that this model filters by URL and title
self.columns_to_filter = [0, 1] 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 # escape to treat a user input % or _ as a literal, not a wildcard
pattern = pattern.replace('%', '\\%') pattern = pattern.replace('%', '\\%')
pattern = pattern.replace('_', '\\_') pattern = pattern.replace('_', '\\_')
# treat spaces as wildcards to match any of the typed words words = ['%{}%'.format(w) for w in pattern.split(' ')]
pattern = re.sub(r' +', '%', pattern)
pattern = '%{}%'.format(pattern) # 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'): 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) self.setQuery(self._query)
def removeRows(self, row, _count, _parent=None): def removeRows(self, row, _count, _parent=None):

View File

@ -94,10 +94,11 @@ def session(*, info=None): # pylint: disable=unused-argument
return model return model
def buffer(*, info=None): # pylint: disable=unused-argument def _buffer(skip_win_id=None):
"""A model to complete on open tabs across all windows. """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): def delete_buffer(data):
"""Close the selected tab.""" """Close the selected tab."""
@ -109,6 +110,8 @@ def buffer(*, info=None): # pylint: disable=unused-argument
model = completionmodel.CompletionModel(column_widths=(6, 40, 54)) model = completionmodel.CompletionModel(column_widths=(6, 40, 54))
for win_id in objreg.window_registry: 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', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
if tabbed_browser.shutting_down: if tabbed_browser.shutting_down:
@ -126,13 +129,31 @@ def buffer(*, info=None): # pylint: disable=unused-argument
return model 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.""" """A model to complete on all open windows."""
model = completionmodel.CompletionModel(column_widths=(6, 30, 64)) model = completionmodel.CompletionModel(column_widths=(6, 30, 64))
windows = [] windows = []
for win_id in objreg.window_registry: for win_id in objreg.window_registry:
if win_id == info.win_id:
continue
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
tab_titles = (tab.title() for tab in tabbed_browser.widgets()) tab_titles = (tab.title() for tab in tabbed_browser.widgets())

View File

@ -104,13 +104,17 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
if self._function: if self._function:
@functools.wraps(func) @functools.wraps(func)
def wrapper(option=None): def wrapper(option=None):
"""Call the underlying function."""
if self._check_match(option): if self._check_match(option):
return func() return func()
return None
else: else:
@functools.wraps(func) @functools.wraps(func)
def wrapper(wrapper_self, option=None): def wrapper(wrapper_self, option=None):
"""Call the underlying function."""
if self._check_match(option): if self._check_match(option):
return func(wrapper_self) return func(wrapper_self)
return None
return wrapper return wrapper
@ -461,7 +465,8 @@ class ConfigContainer:
def __setattr__(self, attr, value): def __setattr__(self, attr, value):
"""Set the given option in the config.""" """Set the given option in the config."""
if attr.startswith('_'): if attr.startswith('_'):
return super().__setattr__(attr, value) super().__setattr__(attr, value)
return
name = self._join(attr) name = self._join(attr)
with self._handle_error('setting', name): with self._handle_error('setting', name):

View File

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

View File

@ -34,6 +34,9 @@ history_gap_interval:
`:history`. Use -1 to disable separation. `:history`. Use -1 to disable separation.
ignore_case: ignore_case:
renamed: search.ignore_case
search.ignore_case:
type: type:
name: String name: String
valid_values: valid_values:
@ -43,6 +46,11 @@ ignore_case:
default: smart default: smart
desc: When to find text on a page case-insensitively. 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: new_instance_open_target:
type: type:
name: String name: String
@ -79,6 +87,9 @@ new_instance_open_target_window:
When `new_instance_open_target` is not set to `window`, this is ignored. When `new_instance_open_target` is not set to `window`, this is ignored.
session_default_name: session_default_name:
renamed: session.default_name
session.default_name:
type: type:
name: SessionName name: SessionName
none_ok: true none_ok: true
@ -88,6 +99,11 @@ session_default_name:
If this is set to null, the session which was last loaded is saved. 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: backend:
type: type:
name: String name: String
@ -1292,6 +1308,7 @@ tabs.title.format:
- host - host
- private - private
- current_url - current_url
- protocol
none_ok: true none_ok: true
desc: | desc: |
Format to use for the tab title. Format to use for the tab title.
@ -1308,6 +1325,7 @@ tabs.title.format:
* `{backend}`: Either ''webkit'' or ''webengine'' * `{backend}`: Either ''webkit'' or ''webengine''
* `{private}`: Indicates when private mode is enabled. * `{private}`: Indicates when private mode is enabled.
* `{current_url}`: URL of the current web page. * `{current_url}`: URL of the current web page.
* `{protocol}`: Protocol (http/https/...) of the current web page.
tabs.title.format_pinned: tabs.title.format_pinned:
default: '{index}' default: '{index}'
@ -1324,6 +1342,7 @@ tabs.title.format_pinned:
- host - host
- private - private
- current_url - current_url
- protocol
none_ok: true none_ok: true
desc: Format to use for the tab title for pinned tabs. The same placeholders desc: Format to use for the tab title for pinned tabs. The same placeholders
like for `tabs.title.format` are defined. like for `tabs.title.format` are defined.
@ -1467,6 +1486,7 @@ window.title_format:
- backend - backend
- private - private
- current_url - current_url
- protocol
default: '{perc}{title}{title_sep}qutebrowser' default: '{perc}{title}{title_sep}qutebrowser'
desc: | desc: |
Format to use for the window title. The same placeholders like for Format to use for the window title. The same placeholders like for
@ -1520,9 +1540,15 @@ zoom.text_only:
## colors ## colors
colors.completion.fg: colors.completion.fg:
default: white default: ["white", "white", "white"]
type: QtColor type:
desc: Text color of the completion widget. 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: colors.completion.odd.bg:
default: '#444444' default: '#444444'
@ -2287,6 +2313,7 @@ bindings.default:
<Ctrl-C>: completion-item-yank <Ctrl-C>: completion-item-yank
<Ctrl-Shift-C>: completion-item-yank --sel <Ctrl-Shift-C>: completion-item-yank --sel
<Return>: command-accept <Return>: command-accept
<Ctrl-Return>: command-accept --rapid
<Ctrl-B>: rl-backward-char <Ctrl-B>: rl-backward-char
<Ctrl-F>: rl-forward-char <Ctrl-F>: rl-forward-char
<Alt-B>: rl-backward-word <Alt-B>: rl-backward-word

View File

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

View File

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

View File

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

View File

@ -1,2 +1,4 @@
# Upstream Mozilla's code # Upstream Mozilla's code
pac_utils.js 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: env:
browser: true browser: true
@ -21,7 +33,7 @@ rules:
no-extra-parens: off no-extra-parens: off
id-length: ["error", {"exceptions": ["i", "k", "x", "y"]}] id-length: ["error", {"exceptions": ["i", "k", "x", "y"]}]
object-shorthand: "off" object-shorthand: "off"
max-statements: ["error", {"max": 30}] max-statements: ["error", {"max": 40}]
quotes: ["error", "double", {"avoidEscape": true}] quotes: ["error", "double", {"avoidEscape": true}]
object-property-newline: ["error", {"allowMultiplePropertiesPerLine": true}] object-property-newline: ["error", {"allowMultiplePropertiesPerLine": true}]
comma-dangle: ["error", "always-multiline"] comma-dangle: ["error", "always-multiline"]

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

@ -99,12 +99,14 @@ window._qutebrowser.webelem = (function() {
const out = { const out = {
"id": id, "id": id,
"value": elem.value,
"outer_xml": elem.outerHTML,
"rects": [], // Gets filled up later "rects": [], // Gets filled up later
"caret_position": caret_position, "caret_position": caret_position,
}; };
// Deal with various fun things which can happen in form elements
// https://github.com/qutebrowser/qutebrowser/issues/2569 // https://github.com/qutebrowser/qutebrowser/issues/2569
// https://github.com/qutebrowser/qutebrowser/issues/2877
// https://stackoverflow.com/q/22942689/2085149
if (typeof elem.tagName === "string") { if (typeof elem.tagName === "string") {
out.tag_name = elem.tagName; out.tag_name = elem.tagName;
} else if (typeof elem.nodeName === "string") { } else if (typeof elem.nodeName === "string") {
@ -120,6 +122,18 @@ window._qutebrowser.webelem = (function() {
out.class_name = ""; out.class_name = "";
} }
if (typeof elem.value === "string" || typeof elem.value === "number") {
out.value = elem.value;
} else {
out.value = "";
}
if (typeof elem.outerHTML === "string") {
out.outer_xml = elem.outerHTML;
} else {
out.outer_xml = "";
}
if (typeof elem.textContent === "string") { if (typeof elem.textContent === "string") {
out.text = elem.textContent; out.text = elem.textContent;
} else if (typeof elem.text === "string") { } else if (typeof elem.text === "string") {

View File

@ -150,7 +150,8 @@ class BaseKeyParser(QObject):
A (count, command) tuple. A (count, command) tuple.
""" """
if self._supports_count: if self._supports_count:
(countstr, cmd_input) = re.match(r'^(\d*)(.*)', keystring).groups() (countstr, cmd_input) = re.fullmatch(r'(\d*)(.*)',
keystring).groups()
count = int(countstr) if countstr else None count = int(countstr) if countstr else None
if count == 0 and not cmd_input: if count == 0 and not cmd_input:
cmd_input = keystring cmd_input = keystring
@ -213,7 +214,7 @@ class BaseKeyParser(QObject):
elif match == self.Match.other: elif match == self.Match.other:
pass pass
else: else:
raise AssertionError("Invalid match value {!r}".format(match)) raise utils.Unreachable("Invalid match value {!r}".format(match))
return match return match
def _match_key(self, cmd_input): def _match_key(self, cmd_input):

View File

@ -322,10 +322,13 @@ class ModeManager(QObject):
if self.mode is None: if self.mode is None:
# We got events before mode is set, so just pass them through. # We got events before mode is set, so just pass them through.
return False return False
if event.type() == QEvent.KeyPress:
return self._eventFilter_keypress(event) handlers = {
else: QEvent.KeyPress: self._eventFilter_keypress,
return self._eventFilter_keyrelease(event) QEvent.KeyRelease: self._eventFilter_keyrelease,
}
handler = handlers[event.type()]
return handler(event)
@cmdutils.register(instance='mode-manager', scope='window') @cmdutils.register(instance='mode-manager', scope='window')
def clear_keychain(self): def clear_keychain(self):

View File

@ -83,6 +83,7 @@ class NormalKeyParser(keyparser.CommandKeyParser):
return match return match
def set_inhibited_timeout(self, timeout): def set_inhibited_timeout(self, timeout):
"""Ignore keypresses for the given duration."""
if timeout != 0: if timeout != 0:
self._debug_log("Inhibiting the normal mode for {}ms.".format( self._debug_log("Inhibiting the normal mode for {}ms.".format(
timeout)) timeout))
@ -217,7 +218,7 @@ class HintKeyParser(keyparser.CommandKeyParser):
self._last_press = LastPress.none self._last_press = LastPress.none
return True return True
elif match == self.Match.other: elif match == self.Match.other:
pass return None
elif match == self.Match.none: elif match == self.Match.none:
# We couldn't find a keychain so we check if it's a special key. # We couldn't find a keychain so we check if it's a special key.
return self._handle_special_key(e) return self._handle_special_key(e)

View File

@ -30,7 +30,7 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
from qutebrowser.commands import runners, cmdutils from qutebrowser.commands import runners, cmdutils
from qutebrowser.config import config, configfiles from qutebrowser.config import config, configfiles
from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils,
jinja, debug) jinja)
from qutebrowser.mainwindow import messageview, prompt from qutebrowser.mainwindow import messageview, prompt
from qutebrowser.completion import completionwidget, completer from qutebrowser.completion import completionwidget, completer
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
@ -94,16 +94,19 @@ def get_window(via_ipc, force_window=False, force_tab=False,
return window.win_id return window.win_id
def raise_window(window): def raise_window(window, alert=True):
"""Raise the given MainWindow object.""" """Raise the given MainWindow object."""
window.setWindowState(window.windowState() & ~Qt.WindowMinimized) window.setWindowState(window.windowState() & ~Qt.WindowMinimized)
window.setWindowState(window.windowState() | Qt.WindowActive) window.setWindowState(window.windowState() | Qt.WindowActive)
window.raise_() window.raise_()
window.activateWindow() window.activateWindow()
if alert:
QApplication.instance().alert(window) QApplication.instance().alert(window)
def get_target_window(): # WORKAROUND for https://github.com/PyCQA/pylint/issues/1770
def get_target_window(): # pylint: disable=inconsistent-return-statements
"""Get the target window for new tabs, or None if none exist.""" """Get the target window for new tabs, or None if none exist."""
try: try:
win_mode = config.val.new_instance_open_target_window win_mode = config.val.new_instance_open_target_window
@ -131,7 +134,6 @@ class MainWindow(QWidget):
Attributes: Attributes:
status: The StatusBar widget. status: The StatusBar widget.
tabbed_browser: The TabbedBrowser widget. tabbed_browser: The TabbedBrowser widget.
state_before_fullscreen: window state before activation of fullscreen.
_downloadview: The DownloadView widget. _downloadview: The DownloadView widget.
_vbox: The main QVBoxLayout. _vbox: The main QVBoxLayout.
_commandrunner: The main CommandRunner instance. _commandrunner: The main CommandRunner instance.
@ -231,8 +233,6 @@ class MainWindow(QWidget):
objreg.get("app").new_window.emit(self) objreg.get("app").new_window.emit(self)
self.state_before_fullscreen = self.windowState()
def _init_geometry(self, geometry): def _init_geometry(self, geometry):
"""Initialize the window geometry or load it from disk.""" """Initialize the window geometry or load it from disk."""
if geometry is not None: if geometry is not None:
@ -320,7 +320,8 @@ class MainWindow(QWidget):
def _init_completion(self): def _init_completion(self):
self._completion = completionwidget.CompletionView(self.win_id, self) self._completion = completionwidget.CompletionView(self.win_id, self)
cmd = objreg.get('status-command', scope='window', window=self.win_id) cmd = objreg.get('status-command', scope='window', window=self.win_id)
completer_obj = completer.Completer(cmd, self._completion) completer_obj = completer.Completer(cmd=cmd, win_id=self.win_id,
parent=self._completion)
self._completion.selection_changed.connect( self._completion.selection_changed.connect(
completer_obj.on_selection_changed) completer_obj.on_selection_changed)
objreg.register('completion', self._completion, scope='window', objreg.register('completion', self._completion, scope='window',
@ -493,12 +494,9 @@ class MainWindow(QWidget):
def _on_fullscreen_requested(self, on): def _on_fullscreen_requested(self, on):
if not config.val.content.windowed_fullscreen: if not config.val.content.windowed_fullscreen:
if on: if on:
self.state_before_fullscreen = self.windowState() self.setWindowState(self.windowState() | Qt.WindowFullScreen)
self.showFullScreen()
elif self.isFullScreen(): elif self.isFullScreen():
self.setWindowState(self.state_before_fullscreen) self.setWindowState(self.windowState() & ~Qt.WindowFullScreen)
log.misc.debug('on: {}, state before fullscreen: {}'.format(
on, debug.qflags_key(Qt, self.state_before_fullscreen)))
@cmdutils.register(instance='main-window', scope='window') @cmdutils.register(instance='main-window', scope='window')
@pyqtSlot() @pyqtSlot()

View File

@ -171,7 +171,7 @@ class PromptQueue(QObject):
# just queue it up for later. # just queue it up for later.
log.prompt.debug("Adding {} to queue.".format(question)) log.prompt.debug("Adding {} to queue.".format(question))
self._queue.append(question) self._queue.append(question)
return return None
if blocking: if blocking:
# If we're blocking we save the old question on the stack, so we # If we're blocking we save the old question on the stack, so we
@ -207,6 +207,7 @@ class PromptQueue(QObject):
return question.answer return question.answer
else: else:
question.completed.connect(self._pop_later) question.completed.connect(self._pop_later)
return None
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self, mode): def _on_mode_left(self, mode):

View File

@ -26,7 +26,8 @@ from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.misc import cmdhistory, editor from qutebrowser.misc import cmdhistory, editor
from qutebrowser.misc import miscwidgets as misc from qutebrowser.misc import miscwidgets as misc
from qutebrowser.utils import usertypes, log, objreg, message from qutebrowser.utils import usertypes, log, objreg, message, utils
from qutebrowser.config import config
class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@ -66,6 +67,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.cursorPositionChanged.connect(self.update_completion) self.cursorPositionChanged.connect(self.update_completion)
self.textChanged.connect(self.update_completion) self.textChanged.connect(self.update_completion)
self.textChanged.connect(self.updateGeometry) self.textChanged.connect(self.updateGeometry)
self.textChanged.connect(self._incremental_search)
def prefix(self): def prefix(self):
"""Get the currently entered command prefix.""" """Get the currently entered command prefix."""
@ -154,8 +156,12 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@cmdutils.register(instance='status-command', @cmdutils.register(instance='status-command',
modes=[usertypes.KeyMode.command], scope='window') modes=[usertypes.KeyMode.command], scope='window')
def command_accept(self): def command_accept(self, rapid=False):
"""Execute the command currently in the commandline.""" """Execute the command currently in the commandline.
Args:
rapid: Run the command without closing or clearing the command bar.
"""
prefixes = { prefixes = {
':': '', ':': '',
'/': 'search -- ', '/': 'search -- ',
@ -163,7 +169,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
} }
text = self.text() text = self.text()
self.history.append(text) self.history.append(text)
modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept') if not rapid:
modeman.leave(self._win_id, usertypes.KeyMode.command,
'cmd accept')
self.got_cmd[str].emit(prefixes[text[0]] + text[1:]) self.got_cmd[str].emit(prefixes[text[0]] + text[1:])
@cmdutils.register(instance='status-command', scope='window') @cmdutils.register(instance='status-command', scope='window')
@ -213,7 +221,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
elif text[0] in modeparsers.STARTCHARS: elif text[0] in modeparsers.STARTCHARS:
super().set_prompt(text[0]) super().set_prompt(text[0])
else: else:
raise AssertionError("setText got called with invalid text " raise utils.Unreachable("setText got called with invalid text "
"'{}'!".format(text)) "'{}'!".format(text))
super().setText(text) super().setText(text)
@ -224,6 +232,12 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Enter/Shift+Enter/etc. will cause QLineEdit to think it's finished Enter/Shift+Enter/etc. will cause QLineEdit to think it's finished
without command_accept to be called. without command_accept to be called.
""" """
text = self.text()
if text in modeparsers.STARTCHARS and e.key() == Qt.Key_Backspace:
e.accept()
modeman.leave(self._win_id, usertypes.KeyMode.command,
'prefix deleted')
return
if e.key() == Qt.Key_Return: if e.key() == Qt.Key_Return:
e.ignore() e.ignore()
return return
@ -238,3 +252,16 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
text = 'x' text = 'x'
width = self.fontMetrics().width(text) width = self.fontMetrics().width(text)
return QSize(width, height) return QSize(width, height)
@pyqtSlot(str)
def _incremental_search(self, text):
if not config.val.search.incremental:
return
search_prefixes = {
'/': 'search -- ',
'?': 'search -r -- ',
}
if self.prefix() in ['/', '?']:
self.got_cmd[str].emit(search_prefixes[text[0]] + text[1:])

View File

@ -256,7 +256,9 @@ class TabbedBrowser(tabwidget.TabWidget):
for tab in self.widgets(): for tab in self.widgets():
self._remove_tab(tab) self._remove_tab(tab)
def tab_close_prompt_if_pinned(self, tab, force, yes_action): def tab_close_prompt_if_pinned(
self, tab, force, yes_action,
text="Are you sure you want to close a pinned tab?"):
"""Helper method for tab_close. """Helper method for tab_close.
If tab is pinned, prompt. If not, run yes_action. If tab is pinned, prompt. If not, run yes_action.
@ -265,7 +267,7 @@ class TabbedBrowser(tabwidget.TabWidget):
if tab.data.pinned and not force: if tab.data.pinned and not force:
message.confirm_async( message.confirm_async(
title='Pinned Tab', title='Pinned Tab',
text="Are you sure you want to close a pinned tab?", text=text,
yes_action=yes_action, default=False, abort_on=[tab.destroyed]) yes_action=yes_action, default=False, abort_on=[tab.destroyed])
else: else:
yes_action() yes_action()
@ -818,6 +820,7 @@ class TabbedBrowser(tabwidget.TabWidget):
point, url = self._global_marks[key] point, url = self._global_marks[key]
def callback(ok): def callback(ok):
"""Scroll once loading finished."""
if ok: if ok:
self.cur_load_finished.disconnect(callback) self.cur_load_finished.disconnect(callback)
tab.scroller.to_point(point) tab.scroller.to_point(point)

View File

@ -172,14 +172,15 @@ class TabWidget(QTabWidget):
fields['perc'] = '' fields['perc'] = ''
try: try:
fields['host'] = self.tab_url(idx).host() url = self.tab_url(idx)
except qtutils.QtValueError: except qtutils.QtValueError:
fields['host'] = '' fields['host'] = ''
try:
fields['current_url'] = self.tab_url(idx).url()
except qtutils.QtValueError:
fields['current_url'] = '' fields['current_url'] = ''
fields['protocol'] = ''
else:
fields['host'] = url.host()
fields['current_url'] = url.toDisplayString()
fields['protocol'] = url.scheme()
y = tab.scroller.pos_perc()[1] y = tab.scroller.pos_perc()[1]
if y is None: if y is None:

View File

@ -152,7 +152,7 @@ def _show_dialog(*args, **kwargs):
elif status == _Result.restart: elif status == _Result.restart:
quitter.restart() quitter.restart()
else: else:
assert False, status raise utils.Unreachable(status)
sys.exit(usertypes.Exit.err_init) sys.exit(usertypes.Exit.err_init)
@ -199,8 +199,7 @@ def _handle_nouveau_graphics():
buttons=[button], buttons=[button],
) )
# Should never be reached raise utils.Unreachable
assert False
def _handle_wayland(): def _handle_wayland():
@ -239,8 +238,7 @@ def _handle_wayland():
"(based on Chromium). " "(based on Chromium). "
) )
# Should never be reached raise utils.Unreachable
assert False
@attr.s @attr.s
@ -359,11 +357,11 @@ def _check_backend_modules():
html.escape(imports.webengine_error)) html.escape(imports.webengine_error))
) )
# Should never be reached raise utils.Unreachable
assert False
def init(): def init():
"""Check for various issues related to QtWebKit/QtWebEngine."""
_check_backend_modules() _check_backend_modules()
if objects.backend == usertypes.Backend.QtWebEngine: if objects.backend == usertypes.Backend.QtWebEngine:
_handle_ssl_support() _handle_ssl_support()

View File

@ -62,7 +62,7 @@ def parse_fatal_stacktrace(text):
r'(Current )?[Tt]hread [^ ]* \(most recent call first\): *', r'(Current )?[Tt]hread [^ ]* \(most recent call first\): *',
r' File ".*", line \d+ in (?P<func>.*)', r' File ".*", line \d+ in (?P<func>.*)',
] ]
m = re.match('\n'.join(lines), text) m = re.search('\n'.join(lines), text)
if m is None: if m is None:
# We got some invalid text. # We got some invalid text.
return ('', '') return ('', '')
@ -348,7 +348,6 @@ class _CrashDialog(QDialog):
"but you're currently running v{} - please " "but you're currently running v{} - please "
"update!".format(newest, qutebrowser.__version__)) "update!".format(newest, qutebrowser.__version__))
text = '<br/><br/>'.join(lines) text = '<br/><br/>'.join(lines)
self.finish()
msgbox.information(self, "Report successfully sent!", text, msgbox.information(self, "Report successfully sent!", text,
on_finished=self.finish, plain_text=False) on_finished=self.finish, plain_text=False)
@ -365,7 +364,6 @@ class _CrashDialog(QDialog):
"<a href=https://www.qutebrowser.org/>qutebrowser.org</a> " "<a href=https://www.qutebrowser.org/>qutebrowser.org</a> "
"by yourself.".format(msg)) "by yourself.".format(msg))
text = '<br/><br/>'.join(lines) text = '<br/><br/>'.join(lines)
self.finish()
msgbox.information(self, "Report successfully sent!", text, msgbox.information(self, "Report successfully sent!", text,
on_finished=self.finish, plain_text=False) on_finished=self.finish, plain_text=False)

View File

@ -20,13 +20,13 @@
"""Handlers for crashes and OS signals.""" """Handlers for crashes and OS signals."""
import os import os
import os.path
import sys import sys
import bdb import bdb
import pdb # noqa: T002 import pdb # noqa: T002
import signal import signal
import functools import functools
import faulthandler import faulthandler
import os.path
try: try:
# WORKAROUND for segfaults when using pdb in pytest for some reason... # WORKAROUND for segfaults when using pdb in pytest for some reason...
import readline # pylint: disable=unused-import import readline # pylint: disable=unused-import

View File

@ -261,6 +261,7 @@ def init_log(args):
def check_optimize_flag(): def check_optimize_flag():
"""Check whether qutebrowser is running with -OO."""
from qutebrowser.utils import log from qutebrowser.utils import log
if sys.flags.optimize >= 2: if sys.flags.optimize >= 2:
log.init.warning("Running on optimize level higher than 1, " log.init.warning("Running on optimize level higher than 1, "

View File

@ -111,9 +111,11 @@ class ExternalEditor(QObject):
# the file from the external editor, see # the file from the external editor, see
# https://github.com/qutebrowser/qutebrowser/issues/1767 # https://github.com/qutebrowser/qutebrowser/issues/1767
with tempfile.NamedTemporaryFile( with tempfile.NamedTemporaryFile(
# pylint: disable=bad-continuation
mode='w', prefix='qutebrowser-editor-', mode='w', prefix='qutebrowser-editor-',
encoding=config.val.editor.encoding, encoding=config.val.editor.encoding,
delete=False) as fobj: delete=False) as fobj:
# pylint: enable=bad-continuation
if text: if text:
fobj.write(text) fobj.write(text)
self._filename = fobj.name self._filename = fobj.name

View File

@ -19,12 +19,14 @@
"""A QProcess which shows notifications in the GUI.""" """A QProcess which shows notifications in the GUI."""
import locale
import shlex import shlex
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QProcess, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QProcess,
QProcessEnvironment) QProcessEnvironment)
from qutebrowser.utils import message, log from qutebrowser.utils import message, log
from qutebrowser.browser import qutescheme
# A mapping of QProcess::ErrorCode's to human-readable strings. # A mapping of QProcess::ErrorCode's to human-readable strings.
@ -96,6 +98,16 @@ class GUIProcess(QObject):
self._started = False self._started = False
log.procs.debug("Process finished with code {}, status {}.".format( log.procs.debug("Process finished with code {}, status {}.".format(
code, status)) code, status))
encoding = locale.getpreferredencoding(do_setlocale=False)
stderr = bytes(self._proc.readAllStandardError()).decode(
encoding, 'replace')
stdout = bytes(self._proc.readAllStandardOutput()).decode(
encoding, 'replace')
qutescheme.spawn_output = self._spawn_format(code, status,
stdout, stderr)
if status == QProcess.CrashExit: if status == QProcess.CrashExit:
message.error("{} crashed!".format(self._what.capitalize())) message.error("{} crashed!".format(self._what.capitalize()))
elif status == QProcess.NormalExit and code == 0: elif status == QProcess.NormalExit and code == 0:
@ -109,13 +121,22 @@ class GUIProcess(QObject):
message.error("{} exited with status {}, see :messages for " message.error("{} exited with status {}, see :messages for "
"details.".format(self._what.capitalize(), code)) "details.".format(self._what.capitalize(), code))
stderr = bytes(self._proc.readAllStandardError()).decode('utf-8')
stdout = bytes(self._proc.readAllStandardOutput()).decode('utf-8')
if stdout: if stdout:
log.procs.error("Process stdout:\n" + stdout.strip()) log.procs.error("Process stdout:\n" + stdout.strip())
if stderr: if stderr:
log.procs.error("Process stderr:\n" + stderr.strip()) log.procs.error("Process stderr:\n" + stderr.strip())
def _spawn_format(self, code=0, status=0, stdout="", stderr=""):
"""Produce a formatted string for spawn output."""
stdout = (stdout or "(No output)").strip()
stderr = (stderr or "(No output)").strip()
spawn_string = ("Process finished with code {}, status {}\n"
"\nProcess stdout:\n {}"
"\nProcess stderr:\n {}").format(code, status,
stdout, stderr)
return spawn_string
@pyqtSlot() @pyqtSlot()
def on_started(self): def on_started(self):
"""Called when the process started successfully.""" """Called when the process started successfully."""

View File

@ -87,7 +87,7 @@ class KeyHintView(QLabel):
Args: Args:
prefix: The current partial keystring. prefix: The current partial keystring.
""" """
countstr, prefix = re.match(r'^(\d*)(.*)', prefix).groups() countstr, prefix = re.fullmatch(r'(\d*)(.*)', prefix).groups()
if not prefix: if not prefix:
self._show_timer.stop() self._show_timer.stop()
self.hide() self.hide()

View File

@ -241,7 +241,7 @@ class WrapperLayout(QLayout):
self._widget = None self._widget = None
def addItem(self, _widget): def addItem(self, _widget):
raise AssertionError("Should never be called!") raise utils.Unreachable
def sizeHint(self): def sizeHint(self):
return self._widget.sizeHint() return self._widget.sizeHint()
@ -250,7 +250,7 @@ class WrapperLayout(QLayout):
return None return None
def takeAt(self, _index): def takeAt(self, _index):
raise AssertionError("Should never be called!") raise utils.Unreachable
def setGeometry(self, rect): def setGeometry(self, rect):
self._widget.setGeometry(rect) self._widget.setGeometry(rect)

View File

@ -21,6 +21,8 @@
import os import os
import os.path import os.path
import itertools
import urllib
import sip import sip
from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer
@ -205,6 +207,12 @@ class SessionManager(QObject):
for idx, item in enumerate(tab.history): for idx, item in enumerate(tab.history):
qtutils.ensure_valid(item) qtutils.ensure_valid(item)
item_data = self._save_tab_item(tab, idx, item) item_data = self._save_tab_item(tab, idx, item)
if item.url().scheme() == 'qute' and item.url().host() == 'back':
# don't add qute://back to the session file
if item_data.get('active', False) and data['history']:
# mark entry before qute://back as active
data['history'][-1]['active'] = True
else:
data['history'].append(item_data) data['history'].append(item_data)
return data return data
@ -251,7 +259,7 @@ class SessionManager(QObject):
object. object.
""" """
if name is default: if name is default:
name = config.val.session_default_name name = config.val.session.default_name
if name is None: if name is None:
if self._current is not None: if self._current is not None:
name = self._current name = self._current
@ -283,7 +291,7 @@ class SessionManager(QObject):
data = self._last_window_session data = self._last_window_session
if data is None: if data is None:
log.sessions.error("last_window_session is None while saving!") log.sessions.error("last_window_session is None while saving!")
return return None
else: else:
data = self._save_all(only_window=only_window, data = self._save_all(only_window=only_window,
with_private=with_private) with_private=with_private)
@ -323,7 +331,18 @@ class SessionManager(QObject):
def _load_tab(self, new_tab, data): def _load_tab(self, new_tab, data):
"""Load yaml data into a newly opened tab.""" """Load yaml data into a newly opened tab."""
entries = [] entries = []
for histentry in data['history']: lazy_load = []
# use len(data['history'])
# -> dropwhile empty if not session.lazy_session
lazy_index = len(data['history'])
gen = itertools.chain(
itertools.takewhile(lambda _: not lazy_load,
enumerate(data['history'])),
enumerate(lazy_load),
itertools.dropwhile(lambda i: i[0] < lazy_index,
enumerate(data['history'])))
for i, histentry in gen:
user_data = {} user_data = {}
if 'zoom' in data: if 'zoom' in data:
@ -347,6 +366,20 @@ class SessionManager(QObject):
if 'pinned' in histentry: if 'pinned' in histentry:
new_tab.data.pinned = histentry['pinned'] new_tab.data.pinned = histentry['pinned']
if (config.val.session.lazy_restore and
histentry.get('active', False) and
not histentry['url'].startswith('qute://back')):
# remove "active" mark and insert back page marked as active
lazy_index = i + 1
lazy_load.append({
'title': histentry['title'],
'url':
'qute://back#' +
urllib.parse.quote(histentry['title']),
'active': True
})
histentry['active'] = False
active = histentry.get('active', False) active = histentry.get('active', False)
url = QUrl.fromEncoded(histentry['url'].encode('ascii')) url = QUrl.fromEncoded(histentry['url'].encode('ascii'))
if 'original-url' in histentry: if 'original-url' in histentry:
@ -360,6 +393,7 @@ class SessionManager(QObject):
entries.append(entry) entries.append(entry)
if active: if active:
new_tab.title_changed.emit(histentry['title']) new_tab.title_changed.emit(histentry['title'])
try: try:
new_tab.history.load_items(entries) new_tab.history.load_items(entries)
except ValueError as e: except ValueError as e:
@ -388,7 +422,7 @@ class SessionManager(QObject):
window=window.win_id) window=window.win_id)
tab_to_focus = None tab_to_focus = None
for i, tab in enumerate(win['tabs']): for i, tab in enumerate(win['tabs']):
new_tab = tabbed_browser.tabopen() new_tab = tabbed_browser.tabopen(background=False)
self._load_tab(new_tab, tab) self._load_tab(new_tab, tab)
if tab.get('active', False): if tab.get('active', False):
tab_to_focus = i tab_to_focus = i
@ -460,7 +494,7 @@ class SessionManager(QObject):
Args: Args:
name: The name of the session. If not given, the session configured name: The name of the session. If not given, the session configured
in session_default_name is saved. in session.default_name is saved.
current: Save the current session instead of the default. current: Save the current session instead of the default.
quiet: Don't show confirmation message. quiet: Don't show confirmation message.
force: Force saving internal sessions (starting with an underline). force: Force saving internal sessions (starting with an underline).
@ -485,7 +519,9 @@ class SessionManager(QObject):
raise cmdexc.CommandError("Error while saving session: {}" raise cmdexc.CommandError("Error while saving session: {}"
.format(e)) .format(e))
else: else:
if not quiet: if quiet:
log.sessions.debug("Saved session {}.".format(name))
else:
message.info("Saved session {}.".format(name)) message.info("Saved session {}.".format(name))
@cmdutils.register(instance='session-manager') @cmdutils.register(instance='session-manager')

View File

@ -21,7 +21,7 @@
import re import re
from qutebrowser.utils import log from qutebrowser.utils import log, utils
class ShellLexer: class ShellLexer:
@ -117,7 +117,8 @@ class ShellLexer:
else: else:
self.token += nextchar self.token += nextchar
else: else:
raise AssertionError("Invalid state {!r}!".format(self.state)) raise utils.Unreachable(
"Invalid state {!r}!".format(self.state))
if self.state in self.escape and not self.keep: if self.state in self.escape and not self.keep:
self.token += self.state self.token += self.state
if self.token or self.quoted: if self.token or self.quoted:

View File

@ -74,7 +74,8 @@ def get_argparser():
"session even if one would be restored.", "session even if one would be restored.",
action='store_true') action='store_true')
parser.add_argument('--target', choices=['auto', 'tab', 'tab-bg', parser.add_argument('--target', choices=['auto', 'tab', 'tab-bg',
'tab-silent', 'tab-bg-silent', 'window'], 'tab-silent', 'tab-bg-silent',
'window'],
help="How URLs should be opened if there is already a " help="How URLs should be opened if there is already a "
"qutebrowser instance running.") "qutebrowser instance running.")
parser.add_argument('--backend', choices=['webkit', 'webengine'], parser.add_argument('--backend', choices=['webkit', 'webengine'],

View File

@ -184,7 +184,7 @@ def signal_name(sig):
Return: Return:
The cleaned up signal name. The cleaned up signal name.
""" """
m = re.match(r'[0-9]+(.*)\(.*\)', sig.signal) m = re.fullmatch(r'[0-9]+(.*)\(.*\)', sig.signal)
return m.group(1) return m.group(1)
@ -266,6 +266,7 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
def __call__(self, func): def __call__(self, func):
@functools.wraps(func) @functools.wraps(func)
def wrapped(*args, **kwargs): def wrapped(*args, **kwargs):
"""Call the original function."""
with self: with self:
func(*args, **kwargs) func(*args, **kwargs)

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