Merge branch 'master' into jay/cache-tabsize

This commit is contained in:
Jay Kamat 2017-10-20 15:24:22 -04:00
commit f6cc9d53b8
No known key found for this signature in database
GPG Key ID: 5D2E399600F4F7B5
59 changed files with 951 additions and 308 deletions

View File

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

View File

@ -15,19 +15,74 @@ 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.0.2 (unreleased) v1.1.0 (unreleased)
------------------- -------------------
Fixes Added
~~~~~ ~~~~~
- Fixed workaround for black screens with Nvidia cards - 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.
Fixed
~~~~~
- More consistent sizing for favicons with vertical tabs.
- Using `:home` on pinned tabs is now prevented.
- Fix crash with unknown file types loaded via qute://help
- Scrolling performance improvements
Deprecated
~~~~~~~~~~
- `:tab-detach` has been deprecated, as `:tab-give` without argument can be used
instead.
Removed
~~~~~~~
- The long-deprecated `:prompt-yes`, `:prompt-no`, `:paste-primary` and `:paste`
commands have been removed.
v1.0.3 (unreleased)
-------------------
Fixed
~~~~~
- Handle accessing a locked sqlite database gracefully
v1.0.2
------
Fixed
~~~~~
- Fix workaround for black screens or crashes with Nvidia cards
- Handle a filesystem going read-only gracefully
- Fix crash when setting `fonts.monospace`
- Fix list options not being modifyable via `.append()` in `config.py`
- Mark the content.notifications setting as QtWebKit only correctly - Mark the content.notifications setting as QtWebKit only correctly
- Fix wrong rendering of keys like `<back>` in the completion
Changed
~~~~~~~
- Nicer error messages and other minor improvements
v1.0.1 v1.0.1
------ ------
Fixes Fixed
~~~~~ ~~~~~
- Fixed starting after customizing `fonts.tabs` or `fonts.debug_console`. - Fixed starting after customizing `fonts.tabs` or `fonts.debug_console`.
@ -65,6 +120,9 @@ Major changes
the entire browsing history. The default for the entire browsing history. The default for
`completion.web_history_max_items` got changed to `-1` (unlimited). If the `completion.web_history_max_items` got changed to `-1` (unlimited). If the
completion is too slow on your machine, try setting it to a few 1000 items. completion is too slow on your machine, try setting it to a few 1000 items.
- Up/Down now navigates through the command history instead of selecting
completion items. Either use Tab to cycle through the completion, or
https://github.com/qutebrowser/qutebrowser/blob/master/doc/help/configuring.asciidoc#migrating-older-configurations[restore the old behavior].
Added Added
~~~~~ ~~~~~

View File

@ -681,7 +681,6 @@ qutebrowser release
* Adjust `__version_info__` in `qutebrowser/__init__.py`. * Adjust `__version_info__` in `qutebrowser/__init__.py`.
* Update changelog (remove *(unreleased)*). * Update changelog (remove *(unreleased)*).
* Run tests again.
* Commit. * Commit.
* Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`). * Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`).
@ -691,9 +690,9 @@ qutebrowser release
* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones * Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones
as closed. as closed.
* Linux: Run `python3 scripts/dev/build_release.py --upload v1.$x.$y`. * Linux: Run `git checkout v1.$x.$y && python3 scripts/dev/build_release.py --upload v1.$x.$y`.
* Windows: Run `C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v1.X.Y` (replace X/Y by hand). * Windows: Run `git checkout v1.X.Y; C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
* macOS: Run `python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand). * macOS: Run `git checkout v1.X.Y && python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand).
* On server: Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand). * On server: Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).
* Update `qutebrowser-git` PKGBUILD if dependencies/install changed. * Update `qutebrowser-git` PKGBUILD if dependencies/install changed.
* Announce to qutebrowser and qutebrowser-announce mailinglist. * Announce to qutebrowser and qutebrowser-announce mailinglist.

View File

@ -83,13 +83,14 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<stop,stop>>|Stop loading in the current/[count]th tab. |<<stop,stop>>|Stop loading in the current/[count]th tab.
|<<tab-clone,tab-clone>>|Duplicate the current tab. |<<tab-clone,tab-clone>>|Duplicate the current tab.
|<<tab-close,tab-close>>|Close the current/[count]th tab. |<<tab-close,tab-close>>|Close the current/[count]th tab.
|<<tab-detach,tab-detach>>|Detach the current tab to its own window.
|<<tab-focus,tab-focus>>|Select the tab given as argument/[count]. |<<tab-focus,tab-focus>>|Select the tab given as argument/[count].
|<<tab-give,tab-give>>|Give the current tab to a new or existing window if win_id given.
|<<tab-move,tab-move>>|Move the current tab according to the argument and [count]. |<<tab-move,tab-move>>|Move the current tab according to the argument and [count].
|<<tab-next,tab-next>>|Switch to the next tab, or switch [count] tabs forward. |<<tab-next,tab-next>>|Switch to the next tab, or switch [count] tabs forward.
|<<tab-only,tab-only>>|Close all tabs except for the current one. |<<tab-only,tab-only>>|Close all tabs except for the current one.
|<<tab-pin,tab-pin>>|Pin/Unpin the current/[count]th tab. |<<tab-pin,tab-pin>>|Pin/Unpin the current/[count]th tab.
|<<tab-prev,tab-prev>>|Switch to the previous tab, or switch [count] tabs back. |<<tab-prev,tab-prev>>|Switch to the previous tab, or switch [count] tabs back.
|<<tab-take,tab-take>>|Take a tab from another window.
|<<unbind,unbind>>|Unbind a keychain. |<<unbind,unbind>>|Unbind a keychain.
|<<undo,undo>>|Re-open a closed tab. |<<undo,undo>>|Re-open a closed tab.
|<<version,version>>|Show version information. |<<version,version>>|Show version information.
@ -946,10 +947,6 @@ Close the current/[count]th tab.
==== count ==== count
The tab index to close The tab index to close
[[tab-detach]]
=== tab-detach
Detach the current tab to its own window.
[[tab-focus]] [[tab-focus]]
=== tab-focus === tab-focus
Syntax: +:tab-focus ['index']+ Syntax: +:tab-focus ['index']+
@ -967,6 +964,17 @@ If neither count nor index are given, it behaves like tab-next. If both are give
==== count ==== count
The tab index to focus, starting with 1. The tab index to focus, starting with 1.
[[tab-give]]
=== tab-give
Syntax: +:tab-give ['win-id']+
Give the current tab to a new or existing window if win_id given.
If no win_id is given, the tab will get detached into a new window.
==== positional arguments
* +'win-id'+: The window ID of the window to give the current tab to.
[[tab-move]] [[tab-move]]
=== tab-move === tab-move
Syntax: +:tab-move ['index']+ Syntax: +:tab-move ['index']+
@ -1019,6 +1027,16 @@ Switch to the previous tab, or switch [count] tabs back.
==== count ==== count
How many tabs to switch back. How many tabs to switch back.
[[tab-take]]
=== tab-take
Syntax: +:tab-take 'index'+
Take a tab from another window.
==== positional arguments
* +'index'+: The [win_id/]index of the tab to take. Or a substring in which case the closest match will be taken.
[[unbind]] [[unbind]]
=== unbind === unbind
Syntax: +:unbind [*--mode* 'mode'] 'key'+ Syntax: +:unbind [*--mode* 'mode'] 'key'+

View File

@ -18,8 +18,9 @@ the old defaults.
Other changes in default settings: Other changes in default settings:
- `<Up>` and `<Down>` in the completion now navigate through command history - `<Up>` and `<Down>` in the completion now navigate through command history
instead of selecting completion items. You can get back the old behavior by instead of selecting completion items. Use `<Tab>`/`<Shift-Tab>` to cycle
doing: through the completion instead.
You can get back the old behavior by doing:
+ +
---- ----
:bind -f -m command <Up> completion-item-focus prev :bind -f -m command <Up> completion-item-focus prev
@ -237,6 +238,9 @@ that via `import utils` as well.
While it's in some cases possible to import code from the qutebrowser While it's in some cases possible to import code from the qutebrowser
installation, doing so is unsupported and discouraged. installation, doing so is unsupported and discouraged.
To read config data from a different file with `c` and `config` available, you
can use `config.source('otherfile.py')` in your `config.py`.
Getting the config directory Getting the config directory
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -346,15 +350,38 @@ def bind_chained(key, *commands):
bind_chained('<Escape>', 'clear-keychain', 'search') bind_chained('<Escape>', 'clear-keychain', 'search')
---- ----
Avoiding flake8 errors Reading colors from Xresources
^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you use an editor with flake8 integration which complains about `c` and `config` being undefined, you can use: You can use something like this to read colors from an `~/.Xresources` file:
[source,python] [source,python]
---- ----
c = c # noqa: F821 def read_xresources(prefix):
config = config # noqa: F821 props = {}
x = subprocess.run(['xrdb', '-query'], stdout=subprocess.PIPE)
lines = x.stdout.decode().split('\n')
for line in filter(lambda l : l.startswith(prefix), lines):
prop, _, value = line.partition(':\t')
props[prop] = value
return props
xresources = read_xresources('*')
c.colors.statusbar.normal.bg = xresources['*background']
----
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
If you use an editor with flake8 and pylint integration, it may have some
complaints about invalid names, undefined variables, or missing docstrings.
You can silence those with:
[source,python]
----
# pylint: disable=C0111
c = c # noqa: F821 pylint: disable=E0602,C0103
config = config # noqa: F821 pylint: disable=E0602,C0103
---- ----
For type annotation support (note that those imports aren't guaranteed to be For type annotation support (note that those imports aren't guaranteed to be
@ -362,8 +389,9 @@ stable across qutebrowser versions):
[source,python] [source,python]
---- ----
# pylint: disable=C0111
from qutebrowser.config.configfiles import ConfigAPI # noqa: F401 from qutebrowser.config.configfiles import ConfigAPI # noqa: F401
from qutebrowser.config.config import ConfigContainer # noqa: F401 from qutebrowser.config.config import ConfigContainer # noqa: F401
config = config # type: ConfigAPI # noqa: F821 config = config # type: ConfigAPI # noqa: F821 pylint: disable=E0602,C0103
c = c # type: ConfigContainer # noqa: F821 c = c # type: ConfigContainer # noqa: F821 pylint: disable=E0602,C0103
---- ----

View File

@ -70,6 +70,8 @@
|<<colors.statusbar.insert.fg,colors.statusbar.insert.fg>>|Foreground color of the statusbar in insert mode. |<<colors.statusbar.insert.fg,colors.statusbar.insert.fg>>|Foreground color of the statusbar in insert mode.
|<<colors.statusbar.normal.bg,colors.statusbar.normal.bg>>|Background color of the statusbar. |<<colors.statusbar.normal.bg,colors.statusbar.normal.bg>>|Background color of the statusbar.
|<<colors.statusbar.normal.fg,colors.statusbar.normal.fg>>|Foreground color of the statusbar. |<<colors.statusbar.normal.fg,colors.statusbar.normal.fg>>|Foreground color of the statusbar.
|<<colors.statusbar.passthrough.bg,colors.statusbar.passthrough.bg>>|Background color of the statusbar in passthrough mode.
|<<colors.statusbar.passthrough.fg,colors.statusbar.passthrough.fg>>|Foreground color of the statusbar in passthrough mode.
|<<colors.statusbar.private.bg,colors.statusbar.private.bg>>|Background color of the statusbar in private browsing mode. |<<colors.statusbar.private.bg,colors.statusbar.private.bg>>|Background color of the statusbar in private browsing mode.
|<<colors.statusbar.private.fg,colors.statusbar.private.fg>>|Foreground color of the statusbar in private browsing mode. |<<colors.statusbar.private.fg,colors.statusbar.private.fg>>|Foreground color of the statusbar in private browsing mode.
|<<colors.statusbar.progress.bg,colors.statusbar.progress.bg>>|Background color of the progress bar. |<<colors.statusbar.progress.bg,colors.statusbar.progress.bg>>|Background color of the progress bar.
@ -94,13 +96,16 @@
|<<colors.tabs.selected.odd.fg,colors.tabs.selected.odd.fg>>|Foreground color of selected odd tabs. |<<colors.tabs.selected.odd.fg,colors.tabs.selected.odd.fg>>|Foreground color of selected odd tabs.
|<<colors.webpage.bg,colors.webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color) |<<colors.webpage.bg,colors.webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color)
|<<completion.cmd_history_max_items,completion.cmd_history_max_items>>|How many commands to save in the command history. |<<completion.cmd_history_max_items,completion.cmd_history_max_items>>|How many commands to save in the command history.
|<<completion.delay,completion.delay>>|Delay in ms before updating completions after typing a character.
|<<completion.height,completion.height>>|The height of the completion, in px or as percentage of the window. |<<completion.height,completion.height>>|The height of the completion, in px or as percentage of the window.
|<<completion.min_chars,completion.min_chars>>|Minimum amount of characters needed to update completions.
|<<completion.quick,completion.quick>>|Move on to the next part when there's only one possible completion left. |<<completion.quick,completion.quick>>|Move on to the next part when there's only one possible completion left.
|<<completion.scrollbar.padding,completion.scrollbar.padding>>|Padding of scrollbar handle in the completion window (in px). |<<completion.scrollbar.padding,completion.scrollbar.padding>>|Padding of scrollbar handle in the completion window (in px).
|<<completion.scrollbar.width,completion.scrollbar.width>>|Width of the scrollbar in the completion window (in px). |<<completion.scrollbar.width,completion.scrollbar.width>>|Width of the scrollbar in the completion window (in px).
|<<completion.show,completion.show>>|When to show the autocompletion window. |<<completion.show,completion.show>>|When to show the autocompletion window.
|<<completion.shrink,completion.shrink>>|Shrink the completion to be smaller than the configured size if there are no scrollbars. |<<completion.shrink,completion.shrink>>|Shrink the completion to be smaller than the configured size if there are no scrollbars.
|<<completion.timestamp_format,completion.timestamp_format>>|How to format timestamps (e.g. for the history completion). |<<completion.timestamp_format,completion.timestamp_format>>|How to format timestamps (e.g. for the history completion).
|<<completion.use_best_match,completion.use_best_match>>|Whether to execute the best-matching command on a partial match.
|<<completion.web_history_max_items,completion.web_history_max_items>>|How many URLs to show in the web history. |<<completion.web_history_max_items,completion.web_history_max_items>>|How many URLs to show in the web history.
|<<confirm_quit,confirm_quit>>|Whether quitting the application requires a confirmation. |<<confirm_quit,confirm_quit>>|Whether quitting the application requires a confirmation.
|<<content.cache.appcache,content.cache.appcache>>|Whether support for the HTML 5 web application cache feature is enabled. |<<content.cache.appcache,content.cache.appcache>>|Whether support for the HTML 5 web application cache feature is enabled.
@ -204,6 +209,7 @@
|<<input.spatial_navigation,input.spatial_navigation>>|Enable Spatial Navigation. |<<input.spatial_navigation,input.spatial_navigation>>|Enable Spatial Navigation.
|<<keyhint.blacklist,keyhint.blacklist>>|Keychains that shouldn't be shown in the keyhint dialog. |<<keyhint.blacklist,keyhint.blacklist>>|Keychains that shouldn't be shown in the keyhint dialog.
|<<keyhint.delay,keyhint.delay>>|Time from pressing a key to seeing the keyhint dialog (ms). |<<keyhint.delay,keyhint.delay>>|Time from pressing a key to seeing the keyhint dialog (ms).
|<<keyhint.radius,keyhint.radius>>|The rounding radius for the edges of the keyhint dialog.
|<<messages.timeout,messages.timeout>>|Time (in ms) to show messages in the statusbar for. |<<messages.timeout,messages.timeout>>|Time (in ms) to show messages in the statusbar for.
|<<messages.unfocused,messages.unfocused>>|Show messages in unfocused windows. |<<messages.unfocused,messages.unfocused>>|Show messages in unfocused windows.
|<<new_instance_open_target,new_instance_open_target>>|How to open links in an existing instance if a new one is launched. |<<new_instance_open_target,new_instance_open_target>>|How to open links in an existing instance if a new one is launched.
@ -1093,6 +1099,22 @@ Type: <<types,QssColor>>
Default: +pass:[white]+ Default: +pass:[white]+
[[colors.statusbar.passthrough.bg]]
=== colors.statusbar.passthrough.bg
Background color of the statusbar in passthrough mode.
Type: <<types,QssColor>>
Default: +pass:[darkblue]+
[[colors.statusbar.passthrough.fg]]
=== colors.statusbar.passthrough.fg
Foreground color of the statusbar in passthrough mode.
Type: <<types,QssColor>>
Default: +pass:[white]+
[[colors.statusbar.private.bg]] [[colors.statusbar.private.bg]]
=== colors.statusbar.private.bg === colors.statusbar.private.bg
Background color of the statusbar in private browsing mode. Background color of the statusbar in private browsing mode.
@ -1293,6 +1315,14 @@ Type: <<types,Int>>
Default: +pass:[100]+ Default: +pass:[100]+
[[completion.delay]]
=== completion.delay
Delay in ms before updating completions after typing a character.
Type: <<types,Int>>
Default: +pass:[0]+
[[completion.height]] [[completion.height]]
=== completion.height === completion.height
The height of the completion, in px or as percentage of the window. The height of the completion, in px or as percentage of the window.
@ -1301,6 +1331,14 @@ Type: <<types,PercOrInt>>
Default: +pass:[50%]+ Default: +pass:[50%]+
[[completion.min_chars]]
=== completion.min_chars
Minimum amount of characters needed to update completions.
Type: <<types,Int>>
Default: +pass:[1]+
[[completion.quick]] [[completion.quick]]
=== completion.quick === completion.quick
Move on to the next part when there's only one possible completion left. Move on to the next part when there's only one possible completion left.
@ -1355,6 +1393,14 @@ Type: <<types,TimestampTemplate>>
Default: +pass:[%Y-%m-%d]+ Default: +pass:[%Y-%m-%d]+
[[completion.use_best_match]]
=== completion.use_best_match
Whether to execute the best-matching command on a partial match.
Type: <<types,Bool>>
Default: +pass:[false]+
[[completion.web_history_max_items]] [[completion.web_history_max_items]]
=== completion.web_history_max_items === completion.web_history_max_items
How many URLs to show in the web history. How many URLs to show in the web history.
@ -2369,6 +2415,14 @@ Type: <<types,Int>>
Default: +pass:[500]+ Default: +pass:[500]+
[[keyhint.radius]]
=== keyhint.radius
The rounding radius for the edges of the keyhint dialog.
Type: <<types,Int>>
Default: +pass:[6]+
[[messages.timeout]] [[messages.timeout]]
=== messages.timeout === messages.timeout
Time (in ms) to show messages in the statusbar for. Time (in ms) to show messages in the statusbar for.
@ -2792,6 +2846,7 @@ The following placeholders are defined:
* `{host}`: The host of the current web page. * `{host}`: The host of the current web page.
* `{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}` : The url of the current web page.
Type: <<types,FormatString>> Type: <<types,FormatString>>
@ -2928,6 +2983,7 @@ The following placeholders are defined:
* `{host}`: The host of the current web page. * `{host}`: The host of the current web page.
* `{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}` : The url of the current web page.
Type: <<types,FormatString>> Type: <<types,FormatString>>

View File

@ -41,21 +41,17 @@ Debian Stretch / Ubuntu 17.04 and newer
Those versions come with QtWebEngine in the repositories. This makes it possible Those versions come with QtWebEngine in the repositories. This makes it possible
to install qutebrowser via the Debian package. to install qutebrowser via the Debian package.
Install the dependencies via apt-get:
----
# apt install python-tox python3-{lxml,pyqt5,sip,jinja2,pygments,yaml,attr} python3-pyqt5.qt{webengine,quick,opengl,sql} libqt5sql5-sqlite
----
Get the qutebrowser package from the Get the qutebrowser package from the
https://github.com/qutebrowser/qutebrowser/releases[release page] and download https://github.com/qutebrowser/qutebrowser/releases[release page] and download
the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package]. the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package].
(If you are using debian testing you can just use the python3-pypeg2 package from the repos)
Install the packages: Install the packages:
---- ----
# dpkg -i python3-pypeg2_*_all.deb # apt install ./python3-pypeg2_*_all.deb
# dpkg -i qutebrowser_*_all.deb # apt install ./qutebrowser_*_all.deb
---- ----
Some additional hints: Some additional hints:
@ -66,7 +62,7 @@ Some additional hints:
`:help` command: `:help` command:
+ +
---- ----
# apt-get install --no-install-recommends asciidoc source-highlight # apt install --no-install-recommends asciidoc source-highlight
$ python3 scripts/asciidoc2html.py $ python3 scripts/asciidoc2html.py
---- ----
@ -76,7 +72,7 @@ $ python3 scripts/asciidoc2html.py
- 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:
+ +
---- ----
# apt-get install gstreamer1.0-plugins-{bad,base,good,ugly} # apt install gstreamer1.0-plugins-{bad,base,good,ugly}
---- ----
On Fedora On Fedora
@ -221,6 +217,10 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[qutebrows
mailinglist] to get notified on new releases). You can install a newer version mailinglist] to get notified on new releases). You can install a newer version
without uninstalling the older one. without uninstalling the older one.
The binary release ships with a QtWebEngine built without proprietary codec
support. To get support for e.g. h264/h265 videos, you'll need to build
QtWebEngine from source yourself with support for that enabled.
https://chocolatey.org/packages/qutebrowser[Chocolatey package] https://chocolatey.org/packages/qutebrowser[Chocolatey package]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -261,6 +261,10 @@ Note that you'll need to upgrade to new versions manually (subscribe to the
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[qutebrowser-announce https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[qutebrowser-announce
mailinglist] to get notified on new releases). mailinglist] to get notified on new releases).
The binary release ships with a QtWebEngine built without proprietary codec
support. To get support for e.g. h264/h265 videos, you'll need to build
QtWebEngine from source yourself with support for that enabled.
This binary is also available through the This binary is also available through the
https://caskroom.github.io/[Homebrew Cask] package manager: https://caskroom.github.io/[Homebrew Cask] package manager:
@ -355,6 +359,18 @@ 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 `tox -e 'mkvenv-win' instead, however make sure that ONLY
Python3 is in your PATH before running tox. Python3 is in your PATH before running tox.
Building the docs
~~~~~~~~~~~~~~~~~
To build the documentation, install `asciidoc` (note that LaTeX which comes as
optional/recommended dependency with some distributions is not required).
Then, run:
----
$ python3 scripts/asciidoc2html.py
----
Creating a wrapper script Creating a wrapper script
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017 suve <veg@svgames.pl> -->
<component type="desktop">
<id>org.qutebrowser.qutebrowser</id>
<metadata_license>CC-BY-SA-3.0</metadata_license>
<project_license>GPL-3.0</project_license>
<name>qutebrowser</name>
<summary>A keyboard-driven web browser</summary>
<description>
<p>
qutebrowser is a keyboard-focused browser with a minimal GUI.
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl,
and is based on Python and PyQt5.
</p>
</description>
<categories>
<category>Network</category>
<category>WebBrowser</category>
</categories>
<provides>
<binary>qutebrowser</binary>
</provides>
<launchable type="desktop-id">qutebrowser.desktop</launchable>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/main.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/downloads.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/completion.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/hints.png</image>
</screenshot>
</screenshots>
<url type="homepage">https://www.qutebrowser.org</url>
<url type="faq">https://qutebrowser.org/doc/faq.html</url>
<url type="help">https://qutebrowser.org/doc/help/</url>
<url type="bugtracker">https://github.com/qutebrowser/qutebrowser/issues/</url>
<url type="donation">https://github.com/qutebrowser/qutebrowser#donating</url>
</component>

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==36.5.0 setuptools==36.6.0
six==1.11.0 six==1.11.0
wheel==0.30.0 wheel==0.30.0

View File

@ -11,7 +11,7 @@ fields==5.0.0
Flask==0.12.2 Flask==0.12.2
glob2==0.6 glob2==0.6
hunter==2.0.1 hunter==2.0.1
hypothesis==3.32.0 hypothesis==3.33.0
itsdangerous==0.24 itsdangerous==0.24
# Jinja2==2.9.6 # Jinja2==2.9.6
Mako==1.0.7 Mako==1.0.7

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, 1) __version_info__ = (1, 0, 2)
__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

@ -353,6 +353,10 @@ class CommandDispatcher:
Return: Return:
A list of URLs that can be opened. A list of URLs that can be opened.
""" """
if isinstance(url, QUrl):
yield url
return
force_search = False force_search = False
urllist = [u for u in url.split('\n') if u.strip()] urllist = [u for u in url.split('\n') if u.strip()]
if (len(urllist) > 1 and not urlutils.is_url(urllist[0]) and if (len(urllist) > 1 and not urlutils.is_url(urllist[0]) and
@ -514,14 +518,54 @@ class CommandDispatcher:
return newtab return newtab
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_detach(self): @cmdutils.argument('index', completion=miscmodels.buffer)
"""Detach the current tab to its own window.""" def tab_take(self, index):
"""Take a tab from another window.
Args:
index: The [win_id/]index of the tab to take. Or a substring
in which case the closest match will be taken.
"""
tabbed_browser, tab = self._resolve_buffer_index(index)
if tabbed_browser is self._tabbed_browser:
raise cmdexc.CommandError("Can't take a tab from the same window")
self._open(tab.url(), tab=True)
tabbed_browser.close_tab(tab, add_undo=False)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('win_id', completion=miscmodels.window)
def tab_give(self, win_id: int = None):
"""Give the current tab to a new or existing window if win_id given.
If no win_id is given, the tab will get detached into a new window.
Args:
win_id: The window ID of the window to give the current tab to.
"""
if win_id == self._win_id:
raise cmdexc.CommandError("Can't give a tab to the same window")
if win_id is None:
if self._count() < 2: if self._count() < 2:
raise cmdexc.CommandError("Cannot detach one tab.") raise cmdexc.CommandError("Cannot detach from a window with "
url = self._current_url() "only one tab")
self._open(url, window=True)
cur_widget = self._current_widget() tabbed_browser = self._new_tabbed_browser(
self._tabbed_browser.close_tab(cur_widget, add_undo=False) private=self._tabbed_browser.private)
else:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.tabopen(self._current_url())
self._tabbed_browser.close_tab(self._current_widget(), add_undo=False)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window', deprecated='Use :tab-give instead!')
def tab_detach(self):
"""Deprecated way to detach a tab."""
self.tab_give()
def _back_forward(self, tab, bg, window, count, forward): def _back_forward(self, tab, bg, window, count, forward):
"""Helper function for :back/:forward.""" """Helper function for :back/:forward."""
@ -972,63 +1016,13 @@ class CommandDispatcher:
else: else:
raise cmdexc.CommandError("Last tab") raise cmdexc.CommandError("Last tab")
@cmdutils.register(instance='command-dispatcher', scope='window', def _resolve_buffer_index(self, index):
deprecated="Use :open {clipboard}") """Resolve a buffer index to the tabbedbrowser and tab.
def paste(self, sel=False, tab=False, bg=False, window=False):
"""Open a page from the clipboard.
If the pasted text contains newlines, each line gets opened in its own
tab.
Args: Args:
sel: Use the primary selection instead of the clipboard. index: The [win_id/]index of the tab to be selected. Or a substring
tab: Open in a new tab.
bg: Open in a background tab.
window: Open in new window.
"""
force_search = False
if not utils.supports_selection():
sel = False
try:
text = utils.get_clipboard(selection=sel)
except utils.ClipboardError as e:
raise cmdexc.CommandError(e)
text_urls = [u for u in text.split('\n') if u.strip()]
if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and
urlutils.get_path_if_valid(
text_urls[0], check_exists=True) is None):
force_search = True
text_urls = [text]
for i, text_url in enumerate(text_urls):
if not window and i > 0:
tab = False
bg = True
try:
url = urlutils.fuzzy_url(text_url, force_search=force_search)
except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e)
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', completion=miscmodels.buffer)
@cmdutils.argument('count', count=True)
def buffer(self, index=None, count=None):
"""Select tab by index or url/title best match.
Focuses window if necessary when index is given. If both index and
count are given, use count.
Args:
index: The [win_id/]index of the tab to focus. Or a substring
in which case the closest match will be focused. in which case the closest match will be focused.
count: The tab index to focus, starting with 1.
""" """
if count is not None:
index_parts = [count]
elif index is None:
raise cmdexc.CommandError("buffer: Either a count or the argument "
"index must be specified.")
else:
index_parts = index.split('/', 1) index_parts = index.split('/', 1)
try: try:
@ -1066,10 +1060,35 @@ class CommandDispatcher:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"There's no tab with index {}!".format(idx)) "There's no tab with index {}!".format(idx))
window = objreg.window_registry[win_id] return (tabbed_browser, tabbed_browser.widget(idx-1))
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', completion=miscmodels.buffer)
@cmdutils.argument('count', count=True)
def buffer(self, index=None, count=None):
"""Select tab by index or url/title best match.
Focuses window if necessary when index is given. If both index and
count are given, use count.
Args:
index: The [win_id/]index of the tab to focus. Or a substring
in which case the closest match will be focused.
count: The tab index to focus, starting with 1.
"""
if count is None and index is None:
raise cmdexc.CommandError("buffer: Either a count or the argument "
"index must be specified.")
if count is not None:
index = str(count)
tabbed_browser, tab = self._resolve_buffer_index(index)
window = tabbed_browser.window()
window.activateWindow() window.activateWindow()
window.raise_() window.raise_()
tabbed_browser.setCurrentIndex(idx-1) tabbed_browser.setCurrentWidget(tab)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['last']) @cmdutils.argument('index', choices=['last'])
@ -1195,7 +1214,7 @@ class CommandDispatcher:
@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."""
self._current_widget().openurl(config.val.url.start_pages[0]) self.openurl(config.val.url.start_pages[0])
def _run_userscript(self, cmd, *args, verbose=False): def _run_userscript(self, cmd, *args, verbose=False):
"""Run a userscript given as argument. """Run a userscript given as argument.
@ -1624,14 +1643,6 @@ class CommandDispatcher:
except webelem.Error as e: except webelem.Error as e:
raise cmdexc.CommandError(str(e)) raise cmdexc.CommandError(str(e))
@cmdutils.register(instance='command-dispatcher',
deprecated="Use :insert-text {primary}",
modes=[KeyMode.insert], hide=True, scope='window',
backend=usertypes.Backend.QtWebKit)
def paste_primary(self):
"""Paste the primary selection at cursor position."""
self.insert_text(utils.get_clipboard(selection=True, fallback=True))
@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):

View File

@ -29,8 +29,9 @@ import os
import time import time
import urllib.parse import urllib.parse
import textwrap import textwrap
import pkg_resources import mimetypes
import pkg_resources
from PyQt5.QtCore import QUrlQuery, QUrl from PyQt5.QtCore import QUrlQuery, QUrl
import qutebrowser import qutebrowser
@ -323,8 +324,10 @@ def qute_help(url):
"scripts/asciidoc2html.py.") "scripts/asciidoc2html.py.")
path = 'html/doc/{}'.format(urlpath) path = 'html/doc/{}'.format(urlpath)
if urlpath.endswith('.png'): if not urlpath.endswith('.html'):
return 'image/png', utils.read_file(path, binary=True) mimetype, _encoding = mimetypes.guess_type(urlpath)
assert mimetype is not None, url
return mimetype, utils.read_file(path, binary=True)
try: try:
data = utils.read_file(path) data = utils.read_file(path)

View File

@ -36,7 +36,8 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
from qutebrowser.browser import shared from qutebrowser.browser import shared
from qutebrowser.browser.webengine import spell from qutebrowser.browser.webengine import spell
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.utils import utils, standarddir, javascript, qtutils, message from qutebrowser.utils import (utils, standarddir, javascript, qtutils,
message, log)
# The default QWebEngineProfile # The default QWebEngineProfile
default_profile = None default_profile = None
@ -145,6 +146,7 @@ class DictionaryLanguageSetter(DefaultProfileSetter):
raise ValueError("'settings' may not be set with " raise ValueError("'settings' may not be set with "
"DictionaryLanguageSetter!") "DictionaryLanguageSetter!")
filenames = [self._find_installed(code) for code in value] filenames = [self._find_installed(code) for code in value]
log.config.debug("Found dicts: {}".format(filenames))
super()._set([f for f in filenames if f], settings) super()._set([f for f in filenames if f], settings)

View File

@ -24,7 +24,7 @@ import functools
import html as html_utils import html as html_utils
import sip import sip
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl, QTimer from PyQt5.QtCore import 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
@ -293,6 +293,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
def __init__(self, tab, parent=None): def __init__(self, tab, parent=None):
super().__init__(tab, parent) super().__init__(tab, parent)
self._args = objreg.get('args')
self._pos_perc = (0, 0) self._pos_perc = (0, 0)
self._pos_px = QPoint() self._pos_px = QPoint()
self._at_bottom = False self._at_bottom = False
@ -307,39 +308,31 @@ class WebEngineScroller(browsertab.AbstractScroller):
for _ in range(min(count, 5000)): for _ in range(min(count, 5000)):
self._tab.key_press(key, modifier) self._tab.key_press(key, modifier)
@pyqtSlot() @pyqtSlot(QPointF)
def _update_pos(self): def _update_pos(self, pos):
"""Update the scroll position attributes when it changed.""" """Update the scroll position attributes when it changed."""
def update_pos_cb(jsret): self._pos_px = pos.toPoint()
"""Callback after getting scroll position via JS.""" contents_size = self._widget.page().contentsSize()
if jsret is None:
# This can happen when the callback would get called after
# shutting down a tab
return
log.webview.vdebug(jsret)
assert isinstance(jsret, dict), jsret
self._pos_px = QPoint(jsret['px']['x'], jsret['px']['y'])
dx = jsret['scroll']['width'] - jsret['inner']['width'] scrollable_x = contents_size.width() - self._widget.width()
if dx == 0: if scrollable_x == 0:
perc_x = 0 perc_x = 0
else: else:
perc_x = min(100, round(100 / dx * jsret['px']['x'])) perc_x = min(100, round(100 / scrollable_x * pos.x()))
dy = jsret['scroll']['height'] - jsret['inner']['height'] scrollable_y = contents_size.height() - self._widget.height()
if dy == 0: if scrollable_y == 0:
perc_y = 0 perc_y = 0
else: else:
perc_y = min(100, round(100 / dy * jsret['px']['y'])) perc_y = min(100, round(100 / scrollable_y * pos.y()))
self._at_bottom = math.ceil(jsret['px']['y']) >= dy self._at_bottom = math.ceil(pos.y()) >= scrollable_y
if (self._pos_perc != (perc_x, perc_y) or
'no-scroll-filtering' in self._args.debug_flags):
self._pos_perc = perc_x, perc_y self._pos_perc = perc_x, perc_y
self.perc_changed.emit(*self._pos_perc) self.perc_changed.emit(*self._pos_perc)
js_code = javascript.assemble('scroll', 'pos')
self._tab.run_js_async(js_code, update_pos_cb)
def pos_px(self): def pos_px(self):
return self._pos_px return self._pos_px

View File

@ -422,12 +422,13 @@ class WebKitScroller(browsertab.AbstractScroller):
else: else:
for val, orientation in [(x, Qt.Horizontal), (y, Qt.Vertical)]: for val, orientation in [(x, Qt.Horizontal), (y, Qt.Vertical)]:
if val is not None: if val is not None:
val = qtutils.check_overflow(val, 'int', fatal=False)
frame = self._widget.page().mainFrame() frame = self._widget.page().mainFrame()
m = frame.scrollBarMaximum(orientation) maximum = frame.scrollBarMaximum(orientation)
if m == 0: if maximum == 0:
continue continue
frame.setScrollBarValue(orientation, int(m * val / 100)) pos = int(maximum * val / 100)
pos = qtutils.check_overflow(pos, 'int', fatal=False)
frame.setScrollBarValue(orientation, pos)
def _key_press(self, key, count=1, getter_name=None, direction=None): def _key_press(self, key, count=1, getter_name=None, direction=None):
frame = self._widget.page().mainFrame() frame = self._widget.page().mainFrame()

View File

@ -214,12 +214,12 @@ class CommandParser:
Return: Return:
cmdstr modified to the matching completion or unmodified cmdstr modified to the matching completion or unmodified
""" """
matches = [] matches = [cmd for cmd in sorted(cmdutils.cmd_dict, key=len)
for valid_command in cmdutils.cmd_dict: if cmdstr in cmd]
if valid_command.find(cmdstr) == 0:
matches.append(valid_command)
if len(matches) == 1: if len(matches) == 1:
cmdstr = matches[0] cmdstr = matches[0]
elif len(matches) > 1 and config.val.completion.use_best_match:
cmdstr = matches[0]
return cmdstr return cmdstr
def _split_args(self, cmd, argstr, keep): def _split_args(self, cmd, argstr, keep):

View File

@ -196,14 +196,25 @@ class Completer(QObject):
For performance reasons we don't want to block here, instead we do this For performance reasons we don't want to block here, instead we do this
in the background. in the background.
We delay the update only if we've already input some text and ignore
updates if the text is shorter than completion.min_chars (unless we're
hitting backspace in which case updates won't be ignored).
""" """
if (self._cmd.cursorPosition() == self._last_cursor_pos and _cmd, _sep, rest = self._cmd.text().partition(' ')
input_length = len(rest)
if (0 < input_length < config.val.completion.min_chars and
self._cmd.cursorPosition() > self._last_cursor_pos):
log.completion.debug("Ignoring update because the length of "
"the text is less than completion.min_chars.")
elif (self._cmd.cursorPosition() == self._last_cursor_pos and
self._cmd.text() == self._last_text): self._cmd.text() == self._last_text):
log.completion.debug("Ignoring update because there were no " log.completion.debug("Ignoring update because there were no "
"changes.") "changes.")
else: else:
log.completion.debug("Scheduling completion update.") log.completion.debug("Scheduling completion update.")
self._timer.start() start_delay = config.val.completion.delay if self._last_text else 0
self._timer.start(start_delay)
self._last_cursor_pos = self._cmd.cursorPosition() self._last_cursor_pos = self._cmd.cursorPosition()
self._last_text = self._cmd.text() self._last_text = self._cmd.text()

View File

@ -202,7 +202,8 @@ class CompletionItemDelegate(QStyledItemDelegate):
if index.column() in columns_to_filter and pattern: if index.column() in columns_to_filter and pattern:
repl = r'<span class="highlight">\g<0></span>' repl = r'<span class="highlight">\g<0></span>'
text = re.sub(re.escape(pattern).replace(r'\ ', r'|'), text = re.sub(re.escape(pattern).replace(r'\ ', r'|'),
repl, self._opt.text, flags=re.IGNORECASE) repl, html.escape(self._opt.text),
flags=re.IGNORECASE)
self._doc.setHtml(text) self._doc.setHtml(text)
else: else:
self._doc.setPlainText(self._opt.text) self._doc.setPlainText(self._opt.text)

View File

@ -122,3 +122,22 @@ def buffer(*, info=None): # pylint: disable=unused-argument
model.add_category(cat) model.add_category(cat)
return model return model
def window(*, info=None): # pylint: disable=unused-argument
"""A model to complete on all open windows."""
model = completionmodel.CompletionModel(column_widths=(6, 30, 64))
windows = []
for win_id in objreg.window_registry:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tab_titles = (tab.title() for tab in tabbed_browser.widgets())
windows.append(("{}".format(win_id),
objreg.window_registry[win_id].windowTitle(),
", ".join(tab_titles)))
model.add_category(listcategory.ListCategory("Windows", windows))
return model

View File

@ -75,6 +75,10 @@ class ConfigCommands:
tabbed_browser.openurl(QUrl('qute://settings'), newtab=False) tabbed_browser.openurl(QUrl('qute://settings'), newtab=False)
return return
if option.endswith('!'):
raise cmdexc.CommandError("Toggling values was moved to the "
":config-cycle command")
if option.endswith('?') and option != '?': if option.endswith('?') and option != '?':
self._print_value(option[:-1]) self._print_value(option[:-1])
return return

View File

@ -678,6 +678,25 @@ completion.web_history_max_items:
0: no history / -1: unlimited 0: no history / -1: unlimited
completion.delay:
default: 0
type:
name: Int
minval: 0
desc: Delay in ms before updating completions after typing a character.
completion.min_chars:
default: 1
type:
name: Int
minval: 1
desc: Minimum amount of characters needed to update completions.
completion.use_best_match:
type: Bool
default: false
desc: Whether to execute the best-matching command on a partial match.
## downloads ## downloads
downloads.location.directory: downloads.location.directory:
@ -959,6 +978,13 @@ keyhint.blacklist:
Globs are supported, so `;*` will blacklist all keychains starting with `;`. Globs are supported, so `;*` will blacklist all keychains starting with `;`.
Use `*` to disable keyhints. Use `*` to disable keyhints.
keyhint.radius:
type:
name: Int
minval: 0
default: 6
desc: The rounding radius for the edges of the keyhint dialog.
# emacs: ' # emacs: '
keyhint.delay: keyhint.delay:
@ -1224,6 +1250,7 @@ tabs.title.format:
- scroll_pos - scroll_pos
- host - host
- private - private
- current_url
none_ok: true none_ok: true
desc: | desc: |
The format to use for the tab title. The format to use for the tab title.
@ -1239,6 +1266,7 @@ tabs.title.format:
* `{host}`: The host of the current web page. * `{host}`: The host of the current web page.
* `{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}` : The url of the current web page.
tabs.title.format_pinned: tabs.title.format_pinned:
default: '{index}' default: '{index}'
@ -1254,6 +1282,7 @@ tabs.title.format_pinned:
- scroll_pos - scroll_pos
- host - host
- private - private
- current_url
none_ok: true none_ok: true
desc: The format to use for the tab title for pinned tabs. The same placeholders desc: The 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.
@ -1371,6 +1400,7 @@ window.title_format:
- host - host
- backend - backend
- private - private
- current_url
default: '{perc}{title}{title_sep}qutebrowser' default: '{perc}{title}{title_sep}qutebrowser'
desc: | desc: |
The format to use for the window title. The format to use for the window title.
@ -1385,6 +1415,7 @@ window.title_format:
* `{host}`: The host of the current web page. * `{host}`: The host of the current web page.
* `{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}` : The url of the current web page.
## zoom ## zoom
@ -1469,16 +1500,6 @@ colors.completion.category.border.bottom:
type: QssColor type: QssColor
desc: Bottom border color of the completion widget category headers. desc: Bottom border color of the completion widget category headers.
colors.statusbar.insert.fg:
default: white
type: QssColor
desc: Foreground color of the statusbar in insert mode.
colors.statusbar.insert.bg:
default: darkgreen
type: QssColor
desc: Background color of the statusbar in insert mode.
colors.completion.item.selected.fg: colors.completion.item.selected.fg:
default: black default: black
type: QtColor type: QtColor
@ -1668,6 +1689,26 @@ colors.statusbar.normal.bg:
type: QssColor type: QssColor
desc: Background color of the statusbar. desc: Background color of the statusbar.
colors.statusbar.insert.fg:
default: white
type: QssColor
desc: Foreground color of the statusbar in insert mode.
colors.statusbar.insert.bg:
default: darkgreen
type: QssColor
desc: Background color of the statusbar in insert mode.
colors.statusbar.passthrough.fg:
default: white
type: QssColor
desc: Foreground color of the statusbar in passthrough mode.
colors.statusbar.passthrough.bg:
default: darkblue
type: QssColor
desc: Background color of the statusbar in passthrough mode.
colors.statusbar.private.fg: colors.statusbar.private.fg:
default: white default: white
type: QssColor type: QssColor

View File

@ -259,6 +259,16 @@ class ConfigAPI:
with self._handle_error('unbinding', key): with self._handle_error('unbinding', key):
self._keyconfig.unbind(key, mode=mode) self._keyconfig.unbind(key, mode=mode)
def source(self, filename):
"""Read the given config file from disk."""
if not os.path.isabs(filename):
filename = str(self.configdir / filename)
try:
read_config_py(filename)
except configexc.ConfigFileErrors as e:
self.errors += e.errors
class ConfigPyWriter: class ConfigPyWriter:

View File

@ -104,7 +104,9 @@ def _update_monospace_fonts():
continue continue
elif not isinstance(opt.typ, configtypes.Font): elif not isinstance(opt.typ, configtypes.Font):
continue continue
elif not config.instance.get_obj(name).endswith(' monospace'):
value = config.instance.get_obj(name)
if value is None or not value.endswith(' monospace'):
continue continue
config.instance.changed.emit(name) config.instance.changed.emit(name)

View File

@ -71,32 +71,5 @@ window._qutebrowser.scroll = (function() {
window.scrollBy(dx, dy); window.scrollBy(dx, dy);
}; };
funcs.pos = function() {
var pos = {
"px": {"x": window.scrollX, "y": window.scrollY},
"scroll": {
"width": Math.max(
document.body.scrollWidth,
document.body.offsetWidth,
document.documentElement.scrollWidth,
document.documentElement.offsetWidth
),
"height": Math.max(
document.body.scrollHeight,
document.body.offsetHeight,
document.documentElement.scrollHeight,
document.documentElement.offsetHeight
),
},
"inner": {
"width": window.innerWidth,
"height": window.innerHeight,
},
};
// console.log(JSON.stringify(pos));
return pos;
};
return funcs; return funcs;
})(); })();

View File

@ -388,20 +388,6 @@ class PromptContainer(QWidget):
message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE) message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE)
question.done() question.done()
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
modes=[usertypes.KeyMode.yesno],
deprecated='Use :prompt-accept yes instead!')
def prompt_yes(self):
"""Answer yes to a yes/no prompt."""
self.prompt_accept('yes')
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
modes=[usertypes.KeyMode.yesno],
deprecated='Use :prompt-accept no instead!')
def prompt_no(self):
"""Answer no to a yes/no prompt."""
self.prompt_accept('no')
@cmdutils.register(instance='prompt-container', hide=True, scope='window', @cmdutils.register(instance='prompt-container', hide=True, scope='window',
modes=[usertypes.KeyMode.prompt], maxsplit=0) modes=[usertypes.KeyMode.prompt], maxsplit=0)
def prompt_open_download(self, cmdline: str = None): def prompt_open_download(self, cmdline: str = None):

View File

@ -43,6 +43,7 @@ class ColorFlags:
command: If we're currently in command mode. command: If we're currently in command mode.
mode: The current caret mode (CaretMode.off/.on/.selection). mode: The current caret mode (CaretMode.off/.on/.selection).
private: Whether this window is in private browsing mode. private: Whether this window is in private browsing mode.
passthrough: If we're currently in passthrough-mode.
""" """
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection']) CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
@ -51,6 +52,7 @@ class ColorFlags:
command = attr.ib(False) command = attr.ib(False)
caret = attr.ib(CaretMode.off) caret = attr.ib(CaretMode.off)
private = attr.ib(False) private = attr.ib(False)
passthrough = attr.ib(False)
def to_stringlist(self): def to_stringlist(self):
"""Get a string list of set flags used in the stylesheet. """Get a string list of set flags used in the stylesheet.
@ -66,6 +68,8 @@ class ColorFlags:
strings.append('command') strings.append('command')
if self.private: if self.private:
strings.append('private') strings.append('private')
if self.passthrough:
strings.append('passthrough')
if self.private and self.command: if self.private and self.command:
strings.append('private-command') strings.append('private-command')
@ -88,6 +92,7 @@ def _generate_stylesheet():
('prompt', 'prompts'), ('prompt', 'prompts'),
('insert', 'statusbar.insert'), ('insert', 'statusbar.insert'),
('command', 'statusbar.command'), ('command', 'statusbar.command'),
('passthrough', 'statusbar.passthrough'),
('private-command', 'statusbar.command.private'), ('private-command', 'statusbar.command.private'),
] ]
stylesheet = """ stylesheet = """
@ -244,6 +249,9 @@ class StatusBar(QWidget):
if mode == usertypes.KeyMode.insert: if mode == usertypes.KeyMode.insert:
log.statusbar.debug("Setting insert flag to {}".format(val)) log.statusbar.debug("Setting insert flag to {}".format(val))
self._color_flags.insert = val self._color_flags.insert = val
if mode == usertypes.KeyMode.passthrough:
log.statusbar.debug("Setting passthrough flag to {}".format(val))
self._color_flags.passthrough = val
if mode == usertypes.KeyMode.command: if mode == usertypes.KeyMode.command:
log.statusbar.debug("Setting command flag to {}".format(val)) log.statusbar.debug("Setting command flag to {}".format(val))
self._color_flags.command = val self._color_flags.command = val
@ -307,7 +315,8 @@ class StatusBar(QWidget):
usertypes.KeyMode.command, usertypes.KeyMode.command,
usertypes.KeyMode.caret, usertypes.KeyMode.caret,
usertypes.KeyMode.prompt, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]: usertypes.KeyMode.yesno,
usertypes.KeyMode.passthrough]:
self.set_mode_active(mode, True) self.set_mode_active(mode, True)
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
@ -324,7 +333,8 @@ class StatusBar(QWidget):
usertypes.KeyMode.command, usertypes.KeyMode.command,
usertypes.KeyMode.caret, usertypes.KeyMode.caret,
usertypes.KeyMode.prompt, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]: usertypes.KeyMode.yesno,
usertypes.KeyMode.passthrough]:
self.set_mode_active(old_mode, False) self.set_mode_active(old_mode, False)
@pyqtSlot(browsertab.AbstractTab) @pyqtSlot(browsertab.AbstractTab)

View File

@ -173,8 +173,18 @@ class TabbedBrowser(tabwidget.TabWidget):
widgets.append(widget) widgets.append(widget)
return widgets return widgets
def _update_window_title(self): def _update_window_title(self, field=None):
"""Change the window title to match the current tab.""" """Change the window title to match the current tab.
Args:
idx: The tab index to update.
field: A field name which was updated. If given, the title
is only set if the given field is in the template.
"""
title_format = config.val.window.title_format
if field is not None and ('{' + field + '}') not in title_format:
return
idx = self.currentIndex() idx = self.currentIndex()
if idx == -1: if idx == -1:
# (e.g. last tab removed) # (e.g. last tab removed)
@ -183,7 +193,6 @@ class TabbedBrowser(tabwidget.TabWidget):
fields = self.get_tab_fields(idx) fields = self.get_tab_fields(idx)
fields['id'] = self._win_id fields['id'] = self._win_id
title_format = config.val.window.title_format
title = title_format.format(**fields) title = title_format.format(**fields)
self.window().setWindowTitle(title) self.window().setWindowTitle(title)
@ -696,8 +705,8 @@ class TabbedBrowser(tabwidget.TabWidget):
log.webview.debug("Not updating scroll position because index is " log.webview.debug("Not updating scroll position because index is "
"-1") "-1")
return return
self._update_window_title() self._update_window_title('scroll_pos')
self._update_tab_title(idx) self._update_tab_title(idx, 'scroll_pos')
def _on_renderer_process_terminated(self, tab, status, code): def _on_renderer_process_terminated(self, tab, status, code):
"""Show an error when a renderer process terminated.""" """Show an error when a renderer process terminated."""

View File

@ -121,21 +121,29 @@ class TabWidget(QTabWidget):
"""Get the tab title user data.""" """Get the tab title user data."""
return self.tabBar().page_title(idx) return self.tabBar().page_title(idx)
def _update_tab_title(self, idx): def _update_tab_title(self, idx, field=None):
"""Update the tab text for the given tab.""" """Update the tab text for the given tab.
Args:
idx: The tab index to update.
field: A field name which was updated. If given, the title
is only set if the given field is in the template.
"""
tab = self.widget(idx) tab = self.widget(idx)
if tab.data.pinned:
fmt = config.val.tabs.title.format_pinned
else:
fmt = config.val.tabs.title.format
if (field is not None and
(fmt is None or ('{' + field + '}') not in fmt)):
return
fields = self.get_tab_fields(idx) fields = self.get_tab_fields(idx)
fields['title'] = fields['title'].replace('&', '&&') fields['title'] = fields['title'].replace('&', '&&')
fields['index'] = idx + 1 fields['index'] = idx + 1
fmt = config.val.tabs.title.format
fmt_pinned = config.val.tabs.title.format_pinned
if tab.data.pinned:
title = '' if fmt_pinned is None else fmt_pinned.format(**fields)
else:
title = '' if fmt is None else fmt.format(**fields) title = '' if fmt is None else fmt.format(**fields)
self.tabBar().setTabText(idx, title) self.tabBar().setTabText(idx, title)
def get_tab_fields(self, idx): def get_tab_fields(self, idx):
@ -164,6 +172,11 @@ class TabWidget(QTabWidget):
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'] = ''
y = tab.scroller.pos_perc()[1] y = tab.scroller.pos_perc()[1]
if y is None: if y is None:
scroll_pos = '???' scroll_pos = '???'
@ -659,7 +672,7 @@ class TabBarStyle(QCommonStyle):
icon_state = (QIcon.On if opt.state & QStyle.State_Selected icon_state = (QIcon.On if opt.state & QStyle.State_Selected
else QIcon.Off) else QIcon.Off)
icon = opt.icon.pixmap(opt.iconSize, icon_mode, icon_state) icon = opt.icon.pixmap(opt.iconSize, icon_mode, icon_state)
p.drawPixmap(layouts.icon.x(), layouts.icon.y(), icon) self._style.drawItemPixmap(p, layouts.icon, Qt.AlignCenter, icon)
def drawControl(self, element, opt, p, widget=None): def drawControl(self, element, opt, p, widget=None):
"""Override drawControl to draw odd tabs in a different color. """Override drawControl to draw odd tabs in a different color.
@ -826,8 +839,7 @@ class TabBarStyle(QCommonStyle):
else QIcon.Off) else QIcon.Off)
# reserve space for favicon when tab bar is vertical (issue #1968) # reserve space for favicon when tab bar is vertical (issue #1968)
position = config.val.tabs.position position = config.val.tabs.position
if (opt.icon.isNull() and if (position in [QTabWidget.East, QTabWidget.West] and
position in [QTabWidget.East, QTabWidget.West] and
config.val.tabs.favicons.show): config.val.tabs.favicons.show):
tab_icon_size = icon_size tab_icon_size = icon_size
else: else:
@ -835,6 +847,7 @@ class TabBarStyle(QCommonStyle):
tab_icon_size = QSize( tab_icon_size = QSize(
min(actual_size.width(), icon_size.width()), min(actual_size.width(), icon_size.width()),
min(actual_size.height(), icon_size.height())) min(actual_size.height(), icon_size.height()))
icon_top = text_rect.center().y() + 1 - tab_icon_size.height() / 2 icon_top = text_rect.center().y() + 1 - tab_icon_size.height() / 2
icon_rect = QRect(QPoint(text_rect.left(), icon_top), tab_icon_size) icon_rect = QRect(QPoint(text_rect.left(), icon_top), tab_icon_size)
icon_rect = self._style.visualRect(opt.direction, opt.rect, icon_rect) icon_rect = self._style.visualRect(opt.direction, opt.rect, icon_rect)

View File

@ -49,7 +49,7 @@ def check_python_version():
# pylint: disable=bad-builtin # pylint: disable=bad-builtin
version_str = '.'.join(map(str, sys.version_info[:3])) version_str = '.'.join(map(str, sys.version_info[:3]))
text = ("At least Python 3.5 is required to run qutebrowser, but " + text = ("At least Python 3.5 is required to run qutebrowser, but " +
version_str + " is installed!\n") "it's running with " + version_str + ".\n")
if Tk and '--no-err-windows' not in sys.argv: # pragma: no cover if Tk and '--no-err-windows' not in sys.argv: # pragma: no cover
root = Tk() root = Tk()
root.withdraw() root.withdraw()

View File

@ -54,9 +54,9 @@ class KeyHintView(QLabel):
background-color: {{ conf.colors.keyhint.bg }}; background-color: {{ conf.colors.keyhint.bg }};
padding: 6px; padding: 6px;
{% if conf.statusbar.position == 'top' %} {% if conf.statusbar.position == 'top' %}
border-bottom-right-radius: 6px; border-bottom-right-radius: {{ conf.keyhint.radius }}px;
{% else %} {% else %}
border-top-right-radius: 6px; border-top-right-radius: {{ conf.keyhint.radius }}px;
{% endif %} {% endif %}
} }
""" """

View File

@ -65,13 +65,12 @@ class SqliteError(SqlError):
log.sql.debug("error code: {}".format(error.nativeErrorCode())) log.sql.debug("error code: {}".format(error.nativeErrorCode()))
# https://sqlite.org/rescode.html # https://sqlite.org/rescode.html
environmental_errors = [
# SQLITE_LOCKED,
# https://github.com/qutebrowser/qutebrowser/issues/2930 # https://github.com/qutebrowser/qutebrowser/issues/2930
'9',
# SQLITE_FULL,
# https://github.com/qutebrowser/qutebrowser/issues/3004 # https://github.com/qutebrowser/qutebrowser/issues/3004
'13', environmental_errors = [
'5', # SQLITE_BUSY ("database is locked")
'8', # SQLITE_READONLY
'13', # SQLITE_FULL
] ]
self.environmental = error.nativeErrorCode() in environmental_errors self.environmental = error.nativeErrorCode() in environmental_errors

View File

@ -159,7 +159,8 @@ def debug_flag_error(flag):
debug-exit: Turn on debugging of late exit. debug-exit: Turn on debugging of late exit.
pdb-postmortem: Drop into pdb on exceptions. pdb-postmortem: Drop into pdb on exceptions.
""" """
valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history'] valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history',
'no-scroll-filtering']
if flag in valid_flags: if flag in valid_flags:
return flag return flag

View File

@ -20,6 +20,7 @@
"""Utilities to get and initialize data/config paths.""" """Utilities to get and initialize data/config paths."""
import os import os
import sys
import shutil import shutil
import os.path import os.path
import contextlib import contextlib
@ -106,6 +107,10 @@ def _init_data(args):
if utils.is_windows: if utils.is_windows:
app_data_path = _writable_location(QStandardPaths.AppDataLocation) app_data_path = _writable_location(QStandardPaths.AppDataLocation)
path = os.path.join(app_data_path, 'data') path = os.path.join(app_data_path, 'data')
elif sys.platform.startswith('haiku'):
# HaikuOS returns an empty value for AppDataLocation
config_path = _writable_location(QStandardPaths.ConfigLocation)
path = os.path.join(config_path, 'data')
else: else:
path = _writable_location(typ) path = _writable_location(typ)
_create(path) _create(path)

View File

@ -882,7 +882,7 @@ def yaml_load(f):
end = datetime.datetime.now() end = datetime.datetime.now()
delta = (end - start).total_seconds() delta = (end - start).total_seconds()
deadline = 3 if 'CI' in os.environ else 1 deadline = 3 if 'CI' in os.environ else 2
if delta > deadline: # pragma: no cover if delta > deadline: # pragma: no cover
log.misc.warning( log.misc.warning(
"YAML load took unusually long, please report this at " "YAML load took unusually long, please report this at "

View File

@ -150,13 +150,14 @@ def _git_str_subprocess(gitpath):
if not os.path.isdir(os.path.join(gitpath, ".git")): if not os.path.isdir(os.path.join(gitpath, ".git")):
return None return None
try: try:
cid = subprocess.check_output( # https://stackoverflow.com/questions/21017300/21017394#21017394
['git', 'describe', '--tags', '--dirty', '--always'], commit_hash = subprocess.check_output(
['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'],
cwd=gitpath).decode('UTF-8').strip() cwd=gitpath).decode('UTF-8').strip()
date = subprocess.check_output( date = subprocess.check_output(
['git', 'show', '-s', '--format=%ci', 'HEAD'], ['git', 'show', '-s', '--format=%ci', 'HEAD'],
cwd=gitpath).decode('UTF-8').strip() cwd=gitpath).decode('UTF-8').strip()
return '{} ({})'.format(cid, date) return '{} ({})'.format(commit_hash, date)
except (subprocess.CalledProcessError, OSError): except (subprocess.CalledProcessError, OSError):
return None return None

View File

@ -20,8 +20,7 @@
"""Custom astroid checker for config calls.""" """Custom astroid checker for config calls."""
import sys import sys
import os import pathlib
import os.path
import yaml import yaml
import astroid import astroid
@ -30,6 +29,7 @@ from pylint.checkers import utils
OPTIONS = None OPTIONS = None
FAILED_LOAD = False
class ConfigChecker(checkers.BaseChecker): class ConfigChecker(checkers.BaseChecker):
@ -44,6 +44,7 @@ class ConfigChecker(checkers.BaseChecker):
None), None),
} }
priority = -1 priority = -1
printed_warning = False
@utils.check_messages('bad-config-option') @utils.check_messages('bad-config-option')
def visit_attribute(self, node): def visit_attribute(self, node):
@ -58,6 +59,13 @@ class ConfigChecker(checkers.BaseChecker):
def _check_config(self, node, name): def _check_config(self, node, name):
"""Check that we're accessing proper config options.""" """Check that we're accessing proper config options."""
if FAILED_LOAD:
if not ConfigChecker.printed_warning:
print("[WARN] Could not find configdata.yml. Please run "
"pylint from qutebrowser root.", file=sys.stderr)
print("Skipping some checks...", file=sys.stderr)
ConfigChecker.printed_warning = True
return
if name not in OPTIONS: if name not in OPTIONS:
self.add_message('bad-config-option', node=node, args=name) self.add_message('bad-config-option', node=node, args=name)
@ -66,6 +74,11 @@ def register(linter):
"""Register this checker.""" """Register this checker."""
linter.register_checker(ConfigChecker(linter)) linter.register_checker(ConfigChecker(linter))
global OPTIONS global OPTIONS
yaml_file = os.path.join('qutebrowser', 'config', 'configdata.yml') global FAILED_LOAD
with open(yaml_file, 'r', encoding='utf-8') as f: yaml_file = pathlib.Path('qutebrowser') / 'config' / 'configdata.yml'
if not yaml_file.exists():
OPTIONS = None
FAILED_LOAD = True
return
with yaml_file.open(mode='r', encoding='utf-8') as f:
OPTIONS = list(yaml.load(f)) OPTIONS = list(yaml.load(f))

View File

@ -0,0 +1,68 @@
#!/usr/bin/env python3
# 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/>.
"""Show various QStandardPath paths."""
import os
import sys
from PyQt5.QtCore import (QT_VERSION_STR, PYQT_VERSION_STR, qVersion,
QStandardPaths, QCoreApplication)
def print_header():
"""Show system information."""
print("Python {}".format(sys.version))
print("os.name: {}".format(os.name))
print("sys.platform: {}".format(sys.platform))
print()
print("Qt {}, compiled {}".format(qVersion(), QT_VERSION_STR))
print("PyQt {}".format(PYQT_VERSION_STR))
print()
def print_paths():
for name, obj in vars(QStandardPaths).items():
if isinstance(obj, QStandardPaths.StandardLocation):
location = QStandardPaths.writableLocation(obj)
print("{:25} {}".format(name, location))
def main():
print_header()
print("No QApplication")
print("===============")
print()
print_paths()
app = QCoreApplication(sys.argv)
app.setApplicationName("qapp_name")
print()
print("With QApplication")
print("=================")
print()
print_paths()
if __name__ == '__main__':
main()

View File

@ -50,13 +50,14 @@ def _git_str():
if not os.path.isdir(os.path.join(BASEDIR, ".git")): if not os.path.isdir(os.path.join(BASEDIR, ".git")):
return None return None
try: try:
cid = subprocess.check_output( # https://stackoverflow.com/questions/21017300/21017394#21017394
['git', 'describe', '--tags', '--dirty', '--always'], commit_hash = subprocess.check_output(
['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'],
cwd=BASEDIR).decode('UTF-8').strip() cwd=BASEDIR).decode('UTF-8').strip()
date = subprocess.check_output( date = subprocess.check_output(
['git', 'show', '-s', '--format=%ci', 'HEAD'], ['git', 'show', '-s', '--format=%ci', 'HEAD'],
cwd=BASEDIR).decode('UTF-8').strip() cwd=BASEDIR).decode('UTF-8').strip()
return '{} ({})'.format(cid, date) return '{} ({})'.format(commit_hash, date)
except (subprocess.CalledProcessError, OSError): except (subprocess.CalledProcessError, OSError):
return None return None

View File

@ -99,6 +99,8 @@ Feature: Page history
Then the page should contain the plaintext "3.txt" Then the page should contain the plaintext "3.txt"
Then the page should contain the plaintext "4.txt" Then the page should contain the plaintext "4.txt"
# Hangs a lot on AppVeyor
@posix
Scenario: Listing history with qute:history redirect Scenario: Listing history with qute:history redirect
When I open data/numbers/3.txt When I open data/numbers/3.txt
And I open data/numbers/4.txt And I open data/numbers/4.txt

View File

@ -74,12 +74,12 @@ Feature: Invoking a new process
# issue #1060 # issue #1060
Scenario: Using target_window = first-opened after tab-detach Scenario: Using target_window = first-opened after tab-give
When I set new_instance_open_target to tab When I set new_instance_open_target to tab
And I set new_instance_open_target_window to first-opened And I set new_instance_open_target_window to first-opened
And I open data/title.html And I open data/title.html
And I open data/search.html in a new tab And I open data/search.html in a new tab
And I run :tab-detach And I run :tab-give
And I wait until data/search.html is loaded And I wait until data/search.html is loaded
And I open data/hello.txt as a URL And I open data/hello.txt as a URL
Then the session should look like: Then the session should look like:

View File

@ -387,22 +387,6 @@ Feature: Prompts
Then the javascript message "confirm reply: true" should be logged Then the javascript message "confirm reply: true" should be logged
And the error "No default value was set for this question!" should be shown And the error "No default value was set for this question!" should be shown
Scenario: Javascript confirm with deprecated :prompt-yes command
When I open data/prompt/jsconfirm.html
And I run :click-element id button
And I wait for a prompt
And I run :prompt-yes
Then the javascript message "confirm reply: true" should be logged
And the warning "prompt-yes is deprecated - Use :prompt-accept yes instead!" should be shown
Scenario: Javascript confirm with deprecated :prompt-no command
When I open data/prompt/jsconfirm.html
And I run :click-element id button
And I wait for a prompt
And I run :prompt-no
Then the javascript message "confirm reply: false" should be logged
And the warning "prompt-no is deprecated - Use :prompt-accept no instead!" should be shown
# Other # Other
@qtwebengine_skip @qtwebengine_skip

View File

@ -638,28 +638,6 @@ Feature: Tab management
And I run :tab-clone And I run :tab-clone
Then no crash should happen Then no crash should happen
# :tab-detach
Scenario: Detaching a tab
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I run :tab-detach
And I wait until data/numbers/2.txt is loaded
Then the session should look like:
windows:
- tabs:
- history:
- url: about:blank
- url: http://localhost:*/data/numbers/1.txt
- tabs:
- history:
- url: http://localhost:*/data/numbers/2.txt
Scenario: Detach tab from window with only one tab
When I open data/hello.txt
And I run :tab-detach
Then the error "Cannot detach one tab." should be shown
# :undo # :undo
Scenario: Undo without any closed tabs Scenario: Undo without any closed tabs
@ -1010,6 +988,76 @@ Feature: Tab management
And I run :buffer "1/2/3" And I run :buffer "1/2/3"
Then the error "No matching tab for: 1/2/3" should be shown Then the error "No matching tab for: 1/2/3" should be shown
# :tab-take
@xfail_norun # Needs qutewm
Scenario: Take a tab from another window
Given I have a fresh instance
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new window
And I run :tab-take 0/1
Then the session should look like:
windows:
- tabs:
- history:
- url: about:blank
- tabs:
- history:
- url: http://localhost:*/data/numbers/2.txt
- history:
- url: http://localhost:*/data/numbers/1.txt
Scenario: Take a tab from the same window
Given I have a fresh instance
When I open data/numbers/1.txt
And I run :tab-take 0/1
Then the error "Can't take a tab from the same window" should be shown
# :tab-give
@xfail_norun # Needs qutewm
Scenario: Give a tab to another window
Given I have a fresh instance
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new window
And I run :tab-give 0
Then the session should look like:
windows:
- tabs:
- history:
- url: http://localhost:*/data/numbers/1.txt
- history:
- url: http://localhost:*/data/numbers/2.txt
- tabs:
- history:
- url: about:blank
Scenario: Give a tab to the same window
Given I have a fresh instance
When I open data/numbers/1.txt
And I run :tab-give 0
Then the error "Can't give a tab to the same window" should be shown
Scenario: Give a tab to a new window
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I run :tab-give
And I wait until data/numbers/2.txt is loaded
Then the session should look like:
windows:
- tabs:
- history:
- url: about:blank
- url: http://localhost:*/data/numbers/1.txt
- tabs:
- history:
- url: http://localhost:*/data/numbers/2.txt
Scenario: Give a tab from window with only one tab
When I open data/hello.txt
And I run :tab-give
Then the error "Cannot detach from a window with only one tab" should be shown
# Other # Other
Scenario: Using :tab-next after closing last tab (#1448) Scenario: Using :tab-next after closing last tab (#1448)
@ -1149,6 +1197,14 @@ Feature: Tab management
And the following tabs should be open: And the following tabs should be open:
- data/numbers/1.txt (active) (pinned) - data/numbers/1.txt (active) (pinned)
Scenario: :tab-pin open url
When I open data/numbers/1.txt
And I run :tab-pin
And I run :home
Then the message "Tab is pinned!" should be shown
And the following tabs should be open:
- data/numbers/1.txt (active) (pinned)
Scenario: Cloning a pinned tab Scenario: Cloning a pinned tab
When I open data/numbers/1.txt When I open data/numbers/1.txt
And I run :tab-pin And I run :tab-pin

View File

@ -17,10 +17,19 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import pytest
import pytest_bdd as bdd import pytest_bdd as bdd
bdd.scenarios('marks.feature') bdd.scenarios('marks.feature')
@pytest.fixture(autouse=True)
def turn_on_scroll_logging(quteproc):
"""Make sure all scrolling changes are logged."""
quteproc.send_cmd(":debug-pyeval -q objreg.get('args')."
"debug_flags.append('no-scroll-filtering')")
@bdd.then(bdd.parsers.parse("the page should be scrolled to {x} {y}")) @bdd.then(bdd.parsers.parse("the page should be scrolled to {x} {y}"))
def check_y(request, quteproc, x, y): def check_y(request, quteproc, x, y):
data = quteproc.get_session() data = quteproc.get_session()

View File

@ -17,5 +17,14 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import pytest
import pytest_bdd as bdd import pytest_bdd as bdd
bdd.scenarios('scroll.feature') bdd.scenarios('scroll.feature')
@pytest.fixture(autouse=True)
def turn_on_scroll_logging(quteproc):
"""Make sure all scrolling changes are logged."""
quteproc.send_cmd(":debug-pyeval -q objreg.get('args')."
"debug_flags.append('no-scroll-filtering')")

View File

@ -105,6 +105,12 @@ def is_ignored_lowlevel_message(message):
elif (message.startswith('QNetworkProxyFactory: factory 0x') and elif (message.startswith('QNetworkProxyFactory: factory 0x') and
message.endswith(' has returned an empty result set')): message.endswith(' has returned an empty result set')):
return True return True
elif message == ' Error: No such file or directory':
# Qt 5.10 with debug Chromium
# [1016/155149.941048:WARNING:stack_trace_posix.cc(625)] Failed to open
# file: /home/florian/#14687139 (deleted)
# Error: No such file or directory
return True
return False return False
@ -170,6 +176,12 @@ def is_ignored_chromium_message(line):
# WebFrame LEAKED 1 TIMES # WebFrame LEAKED 1 TIMES
'WebFrame LEAKED 1 TIMES', 'WebFrame LEAKED 1 TIMES',
# Qt 5.10 with debug Chromium
# [1016/155149.941048:WARNING:stack_trace_posix.cc(625)] Failed to open
# file: /home/florian/#14687139 (deleted)
# Error: No such file or directory
'Failed to open file: * (deleted)',
# macOS on Travis # macOS on Travis
# [5140:5379:0911/063441.239771:ERROR:mach_port_broker.mm(175)] # [5140:5379:0911/063441.239771:ERROR:mach_port_broker.mm(175)]
# Unknown process 5176 is sending Mach IPC messages! # Unknown process 5176 is sending Mach IPC messages!

View File

@ -60,6 +60,9 @@ class WinRegistryHelper:
registry = attr.ib() registry = attr.ib()
def windowTitle(self):
return 'window title - qutebrowser'
def __init__(self): def __init__(self):
self._ids = [] self._ids = []

View File

@ -131,10 +131,11 @@ class FakeUrl:
"""QUrl stub which provides .path(), isValid() and host().""" """QUrl stub which provides .path(), isValid() and host()."""
def __init__(self, path=None, valid=True, host=None): def __init__(self, path=None, valid=True, host=None, url=None):
self.path = mock.Mock(return_value=path) self.path = mock.Mock(return_value=path)
self.isValid = mock.Mock(returl_value=valid) self.isValid = mock.Mock(returl_value=valid)
self.host = mock.Mock(returl_value=host) self.host = mock.Mock(returl_value=host)
self.url = mock.Mock(return_value=url)
class FakeNetworkReply: class FakeNetworkReply:
@ -377,7 +378,9 @@ class FakeTimer(QObject):
def isSingleShot(self): def isSingleShot(self):
return self._singleshot return self._singleshot
def start(self): def start(self, interval=None):
if interval:
self._interval = interval
self._started = True self._started = True
def stop(self): def stop(self):
@ -396,7 +399,7 @@ class InstaTimer(QObject):
timeout = pyqtSignal() timeout = pyqtSignal()
def start(self): def start(self, interval=None):
self.timeout.emit() self.timeout.emit()
def setSingleShot(self, yes): def setSingleShot(self, yes):
@ -520,6 +523,9 @@ class TabbedBrowserStub(QObject):
def count(self): def count(self):
return len(self.tabs) return len(self.tabs)
def widgets(self):
return self.tabs
def widget(self, i): def widget(self, i):
return self.tabs[i] return self.tabs[i]

View File

@ -146,3 +146,26 @@ class TestHistoryHandler:
url = QUrl("qute://history/data?start_time={}".format(now)) url = QUrl("qute://history/data?start_time={}".format(now))
_mimetype, data = benchmark(qutescheme.qute_history, url) _mimetype, data = benchmark(qutescheme.qute_history, url)
assert len(json.loads(data)) > 1 assert len(json.loads(data)) > 1
class TestHelpHandler:
"""Tests for qute://help."""
@pytest.fixture
def data_patcher(self, monkeypatch):
def _patch(path, data):
def _read_file(name, binary=False):
assert path == name
if binary:
return data
return data.decode('utf-8')
monkeypatch.setattr(qutescheme.utils, 'read_file', _read_file)
return _patch
def test_unknown_file_type(self, data_patcher):
data_patcher('html/doc/foo.bin', b'\xff')
mimetype, data = qutescheme.qute_help(QUrl('qute://help/foo.bin'))
assert mimetype == 'application/octet-stream'
assert data == b'\xff'

View File

@ -31,6 +31,8 @@ QWebElement = pytest.importorskip('PyQt5.QtWebKit').QWebElement
from qutebrowser.browser import webelem from qutebrowser.browser import webelem
from qutebrowser.browser.webkit import webkitelem from qutebrowser.browser.webkit import webkitelem
from qutebrowser.misc import objects
from qutebrowser.utils import usertypes
def get_webelem(geometry=None, frame=None, *, null=False, style=None, def get_webelem(geometry=None, frame=None, *, null=False, style=None,
@ -715,8 +717,10 @@ class TestRectOnView:
@pytest.mark.parametrize('js_rect', [None, {}]) @pytest.mark.parametrize('js_rect', [None, {}])
@pytest.mark.parametrize('zoom_text_only', [True, False]) @pytest.mark.parametrize('zoom_text_only', [True, False])
def test_zoomed(self, stubs, config_stub, js_rect, zoom_text_only): def test_zoomed(self, stubs, config_stub, js_rect, monkeypatch,
zoom_text_only):
"""Make sure the coordinates are adjusted when zoomed.""" """Make sure the coordinates are adjusted when zoomed."""
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
config_stub.val.zoom.text_only = zoom_text_only config_stub.val.zoom.text_only = zoom_text_only
geometry = QRect(10, 10, 4, 4) geometry = QRect(10, 10, 4, 4)
frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100), zoom=0.5) frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100), zoom=0.5)

View File

@ -21,7 +21,7 @@
import pytest import pytest
from qutebrowser.commands import runners, cmdexc from qutebrowser.commands import runners, cmdexc, cmdutils
class TestCommandParser: class TestCommandParser:
@ -66,11 +66,47 @@ class TestCommandParser:
with pytest.raises(cmdexc.NoSuchCommandError): with pytest.raises(cmdexc.NoSuchCommandError):
parser.parse_all(command) parser.parse_all(command)
def test_partial_parsing(self):
class TestCompletions:
"""Tests for completions.use_best_match."""
@pytest.fixture(autouse=True)
def cmdutils_stub(self, monkeypatch, stubs):
"""Patch the cmdutils module to provide fake commands."""
monkeypatch.setattr(cmdutils, 'cmd_dict', {
'one': stubs.FakeCommand(name='one'),
'two': stubs.FakeCommand(name='two'),
'two-foo': stubs.FakeCommand(name='two-foo'),
})
def test_partial_parsing(self, config_stub):
"""Test partial parsing with a runner where it's enabled. """Test partial parsing with a runner where it's enabled.
The same with it being disabled is tested by test_parse_all. The same with it being disabled is tested by test_parse_all.
""" """
parser = runners.CommandParser(partial_match=True) parser = runners.CommandParser(partial_match=True)
result = parser.parse('message-i') result = parser.parse('on')
assert result.cmd.name == 'message-info' assert result.cmd.name == 'one'
def test_dont_use_best_match(self, config_stub):
"""Test multiple completion options with use_best_match set to false.
Should raise NoSuchCommandError
"""
config_stub.val.completion.use_best_match = False
parser = runners.CommandParser(partial_match=True)
with pytest.raises(cmdexc.NoSuchCommandError):
parser.parse('tw')
def test_use_best_match(self, config_stub):
"""Test multiple completion options with use_best_match set to true.
The resulting command should be the best match
"""
config_stub.val.completion.use_best_match = True
parser = runners.CommandParser(partial_match=True)
result = parser.parse('tw')
assert result.cmd.name == 'two'

View File

@ -528,6 +528,30 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
QUrl('https://duckduckgo.com')] QUrl('https://duckduckgo.com')]
def test_window_completion(qtmodeltester, fake_web_tab, tabbed_browser_stubs):
tabbed_browser_stubs[0].tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2)
]
tabbed_browser_stubs[1].tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0)
]
model = miscmodels.window()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
_check_completions(model, {
'Windows': [
('0', 'window title - qutebrowser',
'GitHub, Wikipedia, DuckDuckGo'),
('1', 'window title - qutebrowser', 'ArchWiki')
]
})
def test_setting_option_completion(qtmodeltester, config_stub, def test_setting_option_completion(qtmodeltester, config_stub,
configdata_stub, info): configdata_stub, info):
model = configmodel.option(info=info) model = configmodel.option(info=info)

View File

@ -133,9 +133,9 @@ class TestSet:
"QtWebEngine backend!"): "QtWebEngine backend!"):
commands.set(0, 'content.cookies.accept', 'all') commands.set(0, 'content.cookies.accept', 'all')
@pytest.mark.parametrize('option', ['?', '!', 'url.auto_search']) @pytest.mark.parametrize('option', ['?', 'url.auto_search'])
def test_empty(self, commands, option): def test_empty(self, commands, option):
"""Run ':set ?' / ':set !' / ':set url.auto_search'. """Run ':set ?' / ':set url.auto_search'.
Should show an error. Should show an error.
See https://github.com/qutebrowser/qutebrowser/issues/1109 See https://github.com/qutebrowser/qutebrowser/issues/1109
@ -145,6 +145,16 @@ class TestSet:
"value"): "value"):
commands.set(win_id=0, option=option) commands.set(win_id=0, option=option)
def test_toggle(self, commands):
"""Try toggling a value.
Should show an nicer error.
"""
with pytest.raises(cmdexc.CommandError,
match="Toggling values was moved to the "
":config-cycle command"):
commands.set(win_id=0, option='javascript.enabled!')
def test_invalid(self, commands): def test_invalid(self, commands):
"""Run ':set foo?'. """Run ':set foo?'.

View File

@ -580,6 +580,52 @@ class TestConfigPy:
assert isinstance(error.exception, ZeroDivisionError) assert isinstance(error.exception, ZeroDivisionError)
assert error.traceback is not None assert error.traceback is not None
@pytest.mark.parametrize('location', ['abs', 'rel'])
def test_source(self, tmpdir, confpy, location):
if location == 'abs':
subfile = tmpdir / 'subfile.py'
arg = str(subfile)
else:
subfile = tmpdir / 'config' / 'subfile.py'
arg = 'subfile.py'
subfile.write_text("c.content.javascript.enabled = False",
encoding='utf-8')
confpy.write("config.source({!r})".format(arg))
confpy.read()
assert not config.instance._values['content.javascript.enabled']
def test_source_errors(self, tmpdir, confpy):
subfile = tmpdir / 'config' / 'subfile.py'
subfile.write_text("c.foo = 42", encoding='utf-8')
confpy.write("config.source('subfile.py')")
error = confpy.read(error=True)
assert error.text == "While setting 'foo'"
assert isinstance(error.exception, configexc.NoOptionError)
def test_source_multiple_errors(self, tmpdir, confpy):
subfile = tmpdir / 'config' / 'subfile.py'
subfile.write_text("c.foo = 42", encoding='utf-8')
confpy.write("config.source('subfile.py')", "c.bar = 23")
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
configfiles.read_config_py(confpy.filename)
errors = excinfo.value.errors
assert len(errors) == 2
for error in errors:
assert isinstance(error.exception, configexc.NoOptionError)
def test_source_not_found(self, confpy):
confpy.write("config.source('doesnotexist.py')")
error = confpy.read(error=True)
assert error.text == "Error while reading doesnotexist.py"
assert isinstance(error.exception, FileNotFoundError)
class TestConfigPyWriter: class TestConfigPyWriter:

View File

@ -258,6 +258,15 @@ class TestEarlyInit:
# Font subclass, but doesn't end with "monospace" # Font subclass, but doesn't end with "monospace"
assert 'fonts.web.family.standard' not in changed_options assert 'fonts.web.family.standard' not in changed_options
def test_setting_monospace_fonts_family(self, init_patch, args):
"""Make sure setting fonts.monospace after a family works.
See https://github.com/qutebrowser/qutebrowser/issues/3130
"""
configinit.early_init(args)
config.instance.set_str('fonts.web.family.standard', '')
config.instance.set_str('fonts.monospace', 'Terminus')
def test_force_software_rendering(self, monkeypatch, config_stub): def test_force_software_rendering(self, monkeypatch, config_stub):
"""Setting force_software_rendering should set the environment var.""" """Setting force_software_rendering should set the environment var."""
envvar = 'QT_XCB_FORCE_SOFTWARE_OPENGL' envvar = 'QT_XCB_FORCE_SOFTWARE_OPENGL'

View File

@ -28,8 +28,8 @@ import pytest
from qutebrowser.misc import checkpyver from qutebrowser.misc import checkpyver
TEXT = (r"At least Python 3.5 is required to run qutebrowser, but " TEXT = (r"At least Python 3.5 is required to run qutebrowser, but it's "
r"\d+\.\d+\.\d+ is installed!\n") r"running with \d+\.\d+\.\d+.\n")
@pytest.mark.not_frozen @pytest.mark.not_frozen

View File

@ -39,7 +39,7 @@ def test_sqlerror():
class TestSqliteError: class TestSqliteError:
@pytest.mark.parametrize('error_code, environmental', [ @pytest.mark.parametrize('error_code, environmental', [
('9', True), # SQLITE_LOCKED ('5', True), # SQLITE_BUSY
('19', False), # SQLITE_CONSTRAINT ('19', False), # SQLITE_CONSTRAINT
]) ])
def test_environmental(self, error_code, environmental): def test_environmental(self, error_code, environmental):

View File

@ -103,6 +103,20 @@ def test_fake_windows(tmpdir, monkeypatch, what):
assert func() == str(tmpdir / APPNAME / what) assert func() == str(tmpdir / APPNAME / what)
def test_fake_haiku(tmpdir, monkeypatch):
"""Test getting data dir on HaikuOS."""
locations = {
QStandardPaths.DataLocation: '',
QStandardPaths.ConfigLocation: str(tmpdir / 'config' / APPNAME),
}
monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation',
locations.get)
monkeypatch.setattr(standarddir.sys, 'platform', 'haiku1')
standarddir._init_data(args=None)
assert standarddir.data() == str(tmpdir / 'config' / APPNAME / 'data')
class TestWritableLocation: class TestWritableLocation:
"""Tests for _writable_location.""" """Tests for _writable_location."""

View File

@ -356,7 +356,7 @@ class TestGitStrSubprocess:
def test_real_git(self, git_repo): def test_real_git(self, git_repo):
"""Test with a real git repository.""" """Test with a real git repository."""
ret = version._git_str_subprocess(str(git_repo)) ret = version._git_str_subprocess(str(git_repo))
assert ret == 'foobar (1970-01-01 01:00:00 +0100)' assert ret == '6e4b65a (1970-01-01 01:00:00 +0100)'
def test_missing_dir(self, tmpdir): def test_missing_dir(self, tmpdir):
"""Test with a directory which doesn't exist.""" """Test with a directory which doesn't exist."""