diff --git a/.travis.yml b/.travis.yml index 84f61df7f..8c7175f51 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,12 @@ matrix: env: DOCKER=ubuntu-xenial services: docker - os: osx - env: TESTENV=py35 + env: TESTENV=py35 OSX=elcapitan + osx_image: xcode7.3 + # https://github.com/The-Compiler/qutebrowser/issues/2013 + # - os: osx + # env: TESTENV=py35 OSX=yosemite + # osx_image: xcode6.4 - os: linux env: TESTENV=pylint - os: linux @@ -36,6 +41,10 @@ matrix: env: TESTENV=check-manifest - os: linux env: TESTENV=eslint + allow_failures: + - os: osx + env: TESTENV=py35 OSX=elcapitan + osx_image: xcode7.3 cache: directories: diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 051f86487..09dcca173 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -47,6 +47,8 @@ Added https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API[HTML5 page visibility API] - New `readability` userscript which shows a readable version of a page (using the `readability-lxml` python package) +- New `cast` userscript to show a video on a Google Chromecast +- New `:run-with-count` command which replaces the (undocumented) `:count:command` syntax. Changed ~~~~~~~ @@ -179,6 +181,7 @@ Fixed - Fixed hang when using multiple spaces in a row with the URL completion - Fixed crash when closing a window without focusing it - Userscripts now can access QUTE_FIFO correctly on Windows +- Compatibility with pdfjs v1.6.210 v0.8.3 (unreleased) ------------------- diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index da5dc7025..e7ed06481 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -652,4 +652,4 @@ as closed. * OS X: Run `python3 scripts/dev/build_release.py --upload v0.X.Y` (replace X/Y by hand) * On server: Run `python3 scripts/dev/download_release.py v0.X.Y` (replace X/Y by hand) * Update `qutebrowser-git` PKGBUILD if dependencies/install changed -* Announce to qutebrowser mailinglist +* Announce to qutebrowser and qutebrowser-announce mailinglist diff --git a/README.asciidoc b/README.asciidoc index 8574cb119..86743bc71 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -70,6 +70,9 @@ message to the https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at mailto:qutebrowser@lists.qutebrowser.org[]. +There's also a https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[announce-only mailinglist] +at mailto:qutebrowser-announce@lists.qutebrowser.org[]. + Contributions / Bugs -------------------- @@ -159,10 +162,10 @@ Contributors, sorted by the number of commits in descending order: * Claude * Corentin Julé * meles5 +* Kevin Velghe * Philipp Hansch * Daniel Karbach * Panagiotis Ktistakis -* Kevin Velghe * Artur Shaik * Nathan Isom * Thorsten Wißmann @@ -180,6 +183,7 @@ Contributors, sorted by the number of commits in descending order: * knaggita * Oliver Caldwell * Julian Weigt +* Sebastian Frysztak * Jonas Schürmann * error800 * Michael Hoang @@ -244,6 +248,7 @@ Contributors, sorted by the number of commits in descending order: * Tim Harder * Thiago Barroso Perrotta * Sorokin Alexei +* Simon Désaulniers * Rok Mandeljc * Noah Huesser * Moez Bouhlel diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 5d7cdaf87..7fb91bf6c 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1017,6 +1017,7 @@ How many steps to zoom out. |<>|Remove chars backward from the cursor to the beginning of the line. |<>|Remove chars from the cursor to the beginning of the word. |<>|Paste the most recently deleted text. +|<>|Run a command with the given count. |<>|Scroll the current tab in the given direction. |<>|Scroll the frame page-wise. |<>|Scroll to a specific percentage of the page. @@ -1365,6 +1366,26 @@ Paste the most recently deleted text. This acts like readline's yank. +[[run-with-count]] +=== run-with-count +Syntax: +:run-with-count 'count-arg' 'command'+ + +Run a command with the given count. + +If run_with_count itself is run with a count, it multiplies count_arg. + +==== positional arguments +* +'count-arg'+: The count to pass to the command. +* +'command'+: The command to run, with optional args. + +==== count +The count that run_with_count itself received. + +==== note +* This command does not split arguments after the last argument and handles quotes literally. +* With this command, +;;+ is interpreted literally instead of splitting off a second command. +* This command does not replace variables like +\{url\}+. + [[scroll]] === scroll Syntax: +:scroll 'direction'+ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 8f416d3a4..d738a68ca 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -69,6 +69,7 @@ |<>|Whether to validate SSL handshakes. |<>|Whether to try to pre-fetch DNS entries to speed up browsing. |<>|Set custom headers for qutebrowser HTTP requests. +|<>|Set location of a netrc-file for HTTP authentication. If empty, ~/.netrc is used. |============== .Quick reference for section ``completion'' @@ -808,6 +809,12 @@ Set custom headers for qutebrowser HTTP requests. Default: empty +[[network-netrc-file]] +=== netrc-file +Set location of a netrc-file for HTTP authentication. If empty, ~/.netrc is used. + +Default: empty + == completion Options related to completion and command history. diff --git a/doc/quickstart.asciidoc b/doc/quickstart.asciidoc index 9d5375898..dc1426931 100644 --- a/doc/quickstart.asciidoc +++ b/doc/quickstart.asciidoc @@ -32,7 +32,8 @@ image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding c `scripts/asciidoc2html.py` to generate the documentation. * Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. * Subscribe to -https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] +https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or +https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist]. * Let me know what features you are missing or things that need (even small!) improvements. diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 1da675498..2e1c4762c 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -162,6 +162,8 @@ this program. If not, see . * Website: http://www.qutebrowser.org/ * Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] / https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser +* Announce-only mailinglist: mailto:qutebrowser-announce@lists.qutebrowser.org[] / +https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce * IRC: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on http://freenode.net/[Freenode] * Github: https://github.com/The-Compiler/qutebrowser diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 855a5d7ea..16db859b6 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -19,7 +19,7 @@ pbr==1.10.0 pep8==1.7.0 pep8-naming==0.4.1 pycodestyle==2.0.0 -pydocstyle==1.0.0 # rq.filter: != 1.1.0 +pydocstyle==1.1.1 pyflakes==1.3.0 -pyparsing==2.1.9 +pyparsing==2.1.10 six==1.10.0 diff --git a/misc/requirements/requirements-flake8.txt-raw b/misc/requirements/requirements-flake8.txt-raw index 29e2e7c6f..442ed2d54 100644 --- a/misc/requirements/requirements-flake8.txt-raw +++ b/misc/requirements/requirements-flake8.txt-raw @@ -12,7 +12,7 @@ flake8-tidy-imports flake8-tuple hacking pep8-naming -pydocstyle!=1.1.0 +pydocstyle pyflakes # Pinned to 1.5.7 by hacking otherwise @@ -23,6 +23,3 @@ pep8==1.7.0 # https://github.com/JBKahn/flake8-debugger/issues/5 #@ filter: flake8-debugger != 2.0.0 - -# https://gitlab.com/pycqa/flake8-docstrings/issues/16 -#@ filter: pydocstyle != 1.1.0 diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 8d7d67eb4..ddf8adbb4 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -1,2 +1,2 @@ pip==8.1.2 -setuptools==28.2.0 +setuptools==28.6.0 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index cf095233d..6608275ab 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -8,7 +8,7 @@ decorator==4.0.10 Flask==0.11.1 glob2==0.4.1 httpbin==0.5.0 -hypothesis==3.5.2 +hypothesis==3.5.3 itsdangerous==0.24 # Jinja2==2.8 Mako==1.0.4 @@ -19,7 +19,7 @@ py==1.4.31 pytest==3.0.3 pytest-bdd==2.18.0 pytest-catchlog==1.2.2 -pytest-cov==2.3.1 +pytest-cov==2.4.0 pytest-faulthandler==1.3.0 pytest-instafail==0.3.0 pytest-mock==1.2 @@ -31,7 +31,7 @@ pytest-warnings==0.1.0 pytest-xvfb==0.3.0 six==1.10.0 spark-parser==1.4.0 -uncompyle6==2.8.3 +uncompyle6==2.9.2 vulture==0.10 Werkzeug==0.11.11 -xdis==2.3.1 +xdis==3.1.0 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index f077153ff..acb3c0012 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -2,5 +2,5 @@ pluggy==0.4.0 py==1.4.31 -tox==2.3.1 +tox==2.4.1 virtualenv==15.0.3 diff --git a/misc/userscripts/cast b/misc/userscripts/cast new file mode 100755 index 000000000..fd1bab4d5 --- /dev/null +++ b/misc/userscripts/cast @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# +# Behaviour +# Userscript for qutebrowser which casts the url passed in $1 to the default +# ChromeCast device in the network using the program `castnow` +# +# Usage +# You can launch the script from qutebrowser as follows: +# spawn --userscript ${PATH_TO_FILE} {url} +# +# Then, you can control the chromecast by launching the simple command +# `castnow` in a shell which will connect to the running castnow instance. +# +# For stopping the script, issue the command `pkill -f castnow` which would +# then let the rest of the userscript execute for cleaning temporary file. +# +# Thanks +# This userscript borrows Thorsten Wißmann's javascript code from his `mpv` +# userscript. +# +# Dependencies +# - castnow, https://github.com/xat/castnow +# +# Author +# Simon Désaulniers + +if [ -z "$QUTE_FIFO" ] ; then + cat 1>&2 <&2 + else + echo "message-$cmd '${msg//\'/\\\'}'" >> "$QUTE_FIFO" + fi +} + +js() { +cat < + The video is being cast on your ChromeCast device. +

+

+ In order to restore this particular video + click here. +

+ "; + replacement.style.position = "relative"; + replacement.style.zIndex = "100003000000"; + replacement.style.fontSize = "1rem"; + replacement.style.textAlign = "center"; + replacement.style.verticalAlign = "middle"; + replacement.style.height = "100%"; + replacement.style.background = "#101010"; + replacement.style.color = "white"; + replacement.style.border = "4px dashed #545454"; + replacement.style.padding = "2em"; + replacement.style.margin = "auto"; + App.all_replacements[i] = replacement; + App.backup_videos[i] = video; + video.parentNode.replaceChild(replacement, video); + } + + function restore_video(obj, index) { + obj = App.all_replacements[index]; + video = App.backup_videos[index]; + console.log(video); + obj.parentNode.replaceChild(video, obj); + } + + /** force repainting the video, thanks to: + * http://martinwolf.org/2014/06/10/force-repaint-of-an-element-with-javascript/ + */ + var siteHeader = document.getElementById('header'); + siteHeader.style.display='none'; + siteHeader.offsetHeight; // no need to store this anywhere, the reference is enough + siteHeader.style.display='block'; + +EOF +} + +printjs() { + js | sed 's,//.*$,,' | tr '\n' ' ' +} +echo "jseval -q $(printjs)" >> "$QUTE_FIFO" + +tmpdir=$(mktemp -d) +file_to_cast=${tmpdir}/qutecast + +# kill any running instance of castnow +pkill -f /usr/bin/castnow + +# start youtube download in stream mode (-o -) into temporary file +youtube-dl -qo - "$1" > ${file_to_cast} & +ytdl_pid=$! + +msg info "Casting $1" >> "$QUTE_FIFO" +# start castnow in stream mode to cast on ChromeCast +tail -F "${file_to_cast}" | castnow - + +# cleanup remaining background process and file on disk +kill ${ytdl_pid} +rm -rf ${tmpdir} diff --git a/misc/userscripts/qutebrowser_viewsource b/misc/userscripts/qutebrowser_viewsource index 535855bb3..b528c41e8 100755 --- a/misc/userscripts/qutebrowser_viewsource +++ b/misc/userscripts/qutebrowser_viewsource @@ -24,9 +24,9 @@ # Caveat: Does not use authentication of any kind. Add it in if you want it to. # -path=/tmp/qutebrowser_$(mktemp XXXXXXXX).html +path=$(mktemp --tmpdir qutebrowser_XXXXXXXX.html) -curl "$QUTE_URL" > $path +curl "$QUTE_URL" > "$path" urxvt -e vim "$path" rm "$path" diff --git a/pytest.ini b/pytest.ini index 2b68a77af..0c2d00f21 100644 --- a/pytest.ini +++ b/pytest.ini @@ -30,6 +30,9 @@ qt_log_ignore = ^QProcess: Destroyed while process .* is still running\. ^"Method "GetAll" with signature "s" on interface "org\.freedesktop\.DBus\.Properties" doesn't exist ^"Method \\"GetAll\\" with signature \\"s\\" on interface \\"org\.freedesktop\.DBus\.Properties\\" doesn't exist\\n" + ^propsReply "Method \\"GetAll\\" with signature \\"s\\" on interface \\"org\.freedesktop\.DBus\.Properties\\" doesn't exist\\n" + ^nmReply "Method \\"GetDevices\\" with signature \\"\\" on interface \\"org\.freedesktop\.NetworkManager\\" doesn't exist\\n" + ^"Object path cannot be empty" ^virtual void QSslSocketBackendPrivate::transmit\(\) SSL write failed with error: -9805 ^virtual void QSslSocketBackendPrivate::transmit\(\) SSLRead failed with: -9805 ^Type conversion already registered from type .* diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 345d49b97..68397bbd3 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -50,7 +50,8 @@ from qutebrowser.browser.webkit import cookies, cache, downloads from qutebrowser.browser.webkit.network import (webkitqutescheme, proxy, networkmanager) from qutebrowser.mainwindow import mainwindow -from qutebrowser.misc import readline, ipc, savemanager, sessions, crashsignal +from qutebrowser.misc import (readline, ipc, savemanager, sessions, + crashsignal, earlyinit) from qutebrowser.misc import utilcmds # pylint: disable=unused-import from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, objreg, usertypes, standarddir, error, debug) @@ -198,6 +199,9 @@ def _process_args(args): _open_startpage() _open_quickstart(args) + delta = datetime.datetime.now() - earlyinit.START_TIME + log.init.debug("Init finished after {}s".format(delta.total_seconds())) + def _load_session(name): """Load the default session. diff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py index 51ed2dfbe..3bc2c2dc3 100644 --- a/qutebrowser/browser/pdfjs.py +++ b/qutebrowser/browser/pdfjs.py @@ -62,8 +62,10 @@ def _generate_pdfjs_script(url): url: The url of the pdf page as QUrl. """ return ( - 'PDFJS.verbosity = PDFJS.VERBOSITY_LEVELS.info;\n' - 'PDFView.open("{url}");\n' + 'document.addEventListener("DOMContentLoaded", function() {{\n' + ' PDFJS.verbosity = PDFJS.VERBOSITY_LEVELS.info;\n' + ' (window.PDFView || window.PDFViewerApplication).open("{url}");\n' + '}});\n' ).format(url=javascript.string_escape(url.toString(QUrl.FullyEncoded))) diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index cc9d75d00..41d4e7355 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -37,14 +37,6 @@ from PyQt5.QtCore import QUrl from qutebrowser.browser.webkit import webkitelem, downloads from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils -try: - import cssutils -except (ImportError, re.error): - # Catching re.error because cssutils in earlier releases (<= 1.0) is broken - # on Python 3.5 - # See https://bitbucket.org/cthedot/cssutils/issues/52 - cssutils = None - _File = collections.namedtuple('_File', ['content', 'content_type', 'content_location', 'transfer_encoding']) @@ -85,6 +77,14 @@ def _get_css_imports_cssutils(data, inline=False): data: The content of the stylesheet to scan as string. inline: True if the argument is an inline HTML style attribute. """ + try: + import cssutils + except (ImportError, re.error): + # Catching re.error because cssutils in earlier releases (<= 1.0) is + # broken on Python 3.5 + # See https://bitbucket.org/cthedot/cssutils/issues/52 + return None + # We don't care about invalid CSS data, this will only litter the log # output with CSS errors parser = cssutils.CSSParser(loglevel=100, @@ -114,10 +114,10 @@ def _get_css_imports(data, inline=False): data: The content of the stylesheet to scan as string. inline: True if the argument is an inline HTML style attribute. """ - if cssutils is None: - return _get_css_imports_regex(data) - else: - return _get_css_imports_cssutils(data, inline) + imports = _get_css_imports_cssutils(data, inline) + if imports is None: + imports = _get_css_imports_regex(data) + return imports def _check_rel(element): diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 04ce58975..d28bd6be4 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -330,7 +330,7 @@ class NetworkManager(QNetworkAccessManager): # altogether. reply.netrc_used = True try: - net = netrc.netrc() + net = netrc.netrc(config.get('network', 'netrc-file')) authenticators = net.authenticators(reply.url().host()) if authenticators is not None: (user, _account, password) = authenticators diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 35464cb9f..5d068422e 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -31,8 +31,7 @@ from qutebrowser.utils import message, objreg, qtutils, usertypes, utils from qutebrowser.misc import split -ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline', - 'count']) +ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline']) last_command = {} macro = {} @@ -158,26 +157,6 @@ class CommandRunner(QObject): for sub in sub_texts: yield self.parse(sub, *args, **kwargs) - def _parse_count(self, cmdstr): - """Split a count prefix off from a command for parse(). - - Args: - cmdstr: The command/args including the count. - - Return: - A (count, cmdstr) tuple, with count being None or int. - """ - if ':' not in cmdstr: - return (None, cmdstr) - - count, cmdstr = cmdstr.split(':', maxsplit=1) - try: - count = int(count) - except ValueError: - # We just ignore invalid prefixes - count = None - return (count, cmdstr) - def parse(self, text, *, fallback=False, keep=False): """Split the commandline text into command and arguments. @@ -191,7 +170,6 @@ class CommandRunner(QObject): A ParseResult tuple. """ cmdstr, sep, argstr = text.partition(' ') - count, cmdstr = self._parse_count(cmdstr) if not cmdstr and not fallback: raise cmdexc.NoSuchCommandError("No command given") @@ -206,8 +184,7 @@ class CommandRunner(QObject): raise cmdexc.NoSuchCommandError( '{}: no such command'.format(cmdstr)) cmdline = split.split(text, keep=keep) - return ParseResult(cmd=None, args=None, cmdline=cmdline, - count=count) + return ParseResult(cmd=None, args=None, cmdline=cmdline) args = self._split_args(cmd, argstr, keep) if keep and args: @@ -217,7 +194,7 @@ class CommandRunner(QObject): else: cmdline = [cmdstr] + args[:] - return ParseResult(cmd=cmd, args=args, cmdline=cmdline, count=count) + return ParseResult(cmd=cmd, args=args, cmdline=cmdline) def _completion_match(self, cmdstr): """Replace cmdstr with a matching completion if there's only one match. @@ -295,18 +272,9 @@ class CommandRunner(QObject): args = result.args else: args = replace_variables(self._win_id, result.args) - if count is not None: - if result.count is not None: - raise cmdexc.CommandMetaError("Got count via command and " - "prefix!") - result.cmd.run(self._win_id, args, count=count) - elif result.count is not None: - result.cmd.run(self._win_id, args, count=result.count) - else: - result.cmd.run(self._win_id, args) + result.cmd.run(self._win_id, args, count=count) - parsed_command = (self._parse_count(text)[1], - count if count is not None else result.count) + parsed_command = (text, count) if result.cmdline[0] != 'repeat-command': last_command[cur_mode] = parsed_command diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 21f7b99ce..c5c1915ca 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -157,6 +157,7 @@ class Completer(QObject): result = runner.parse(text, fallback=True, keep=True) parts = [x for x in result.cmdline if x] pos = self._cmd.cursorPosition() - len(self._cmd.prefix()) + pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars log.completion.debug('partitioning {} around position {}'.format(parts, pos)) for i, part in enumerate(parts): diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 672382ae9..5cd9136c6 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -438,6 +438,11 @@ def data(readonly=False): SettingValue(typ.HeaderDict(none_ok=True), ''), "Set custom headers for qutebrowser HTTP requests."), + ('netrc-file', + SettingValue(typ.File(none_ok=True), ''), + "Set location of a netrc-file for HTTP authentication. If empty, " + "~/.netrc is used."), + readonly=readonly )), diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 593ed04bd..ebc4f28ad 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -503,8 +503,8 @@ class MainWindow(QWidget): # Ask if multiple downloads running if 'downloads' in confirm_quit and download_count > 0: quit_texts.append("{} {} running.".format( - tab_count, - "download is" if tab_count == 1 else "downloads are")) + download_count, + "download is" if download_count == 1 else "downloads are")) # Process all quit messages that user must confirm if quit_texts or 'always' in confirm_quit: text = '\n'.join(['Really quit?'] + quit_texts) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index baeb7d620..657a38dc3 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -703,13 +703,10 @@ class TabBarStyle(QCommonStyle): text_rect.adjust(indicator_width + indicator_padding.left + indicator_padding.right, 0, 0, 0) - if opt.icon.isNull(): - icon_rect = QRect() - else: + icon_rect = self._get_icon_rect(opt, text_rect) + if icon_rect.isValid(): icon_padding = self.pixelMetric(PixelMetrics.icon_padding, opt) - icon_rect = self._get_icon_rect(opt, text_rect) - if icon_rect.isValid(): - text_rect.adjust(icon_rect.width() + icon_padding, 0, 0, 0) + text_rect.adjust(icon_rect.width() + icon_padding, 0, 0, 0) text_rect = self._style.visualRect(opt.direction, opt.rect, text_rect) return Layouts(text=text_rect, icon=icon_rect, @@ -733,9 +730,17 @@ class TabBarStyle(QCommonStyle): else QIcon.Disabled) icon_state = (QIcon.On if opt.state & QStyle.State_Selected else QIcon.Off) - tab_icon_size = opt.icon.actualSize(icon_size, icon_mode, icon_state) - tab_icon_size = QSize(min(tab_icon_size.width(), icon_size.width()), - min(tab_icon_size.height(), icon_size.height())) + # reserve space for favicon when tab bar is vertical (issue #1968) + position = config.get('tabs', 'position') + if (opt.icon.isNull() and + position in [QTabWidget.East, QTabWidget.West] and + config.get('tabs', 'show-favicons')): + tab_icon_size = icon_size + else: + actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state) + tab_icon_size = QSize( + min(actual_size.width(), icon_size.width()), + min(actual_size.height(), icon_size.height())) icon_rect = QRect(text_rect.left(), text_rect.top() + 1, tab_icon_size.width(), tab_icon_size.height()) icon_rect = self._style.visualRect(opt.direction, opt.rect, icon_rect) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index dd498edf3..08c933a6e 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -37,6 +37,7 @@ import signal import operator import importlib import pkg_resources +import datetime try: import tkinter except ImportError: @@ -45,6 +46,9 @@ except ImportError: # initialization needs to take place before that! +START_TIME = datetime.datetime.now() + + def _missing_str(name, *, windows=None, pip=None, webengine=False): """Get an error string for missing packages. diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index cd3d2f3bc..c3a3a8e34 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -481,3 +481,5 @@ class SessionManager(QObject): log.sessions.exception("Error while deleting session!") raise cmdexc.CommandError("Error while deleting session: {}" .format(e)) + else: + log.sessions.debug("Deleted session {}.".format(name)) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 010d9f33b..4213dbc97 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -86,6 +86,23 @@ def repeat(times: int, command, win_id): commandrunner.run_safely(command) +@cmdutils.register(maxsplit=1, hide=True, no_cmd_split=True, + no_replace_variables=True) +@cmdutils.argument('win_id', win_id=True) +@cmdutils.argument('count', count=True) +def run_with_count(count_arg: int, command, win_id, count=1): + """Run a command with the given count. + + If run_with_count itself is run with a count, it multiplies count_arg. + + Args: + count_arg: The count to pass to the command. + command: The command to run, with optional args. + count: The count that run_with_count itself received. + """ + runners.CommandRunner(win_id).run(command, count_arg * count) + + @cmdutils.register(hide=True) def message_error(text): """Show an error message in the statusbar. diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index c88df8a38..044b75b65 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -148,7 +148,11 @@ console_filter = None def stub(suffix=''): """Show a STUB: message for the calling function.""" - function = inspect.stack()[1][3] + try: + function = inspect.stack()[1][3] + except IndexError: # pragma: no cover + misc.exception("Failed to get stack") + function = '' text = "STUB: {}".format(function) if suffix: text = '{} ({})'.format(text, suffix) diff --git a/scripts/dev/ci/travis_install.sh b/scripts/dev/ci/travis_install.sh index 435af6389..4edb587a4 100644 --- a/scripts/dev/ci/travis_install.sh +++ b/scripts/dev/ci/travis_install.sh @@ -88,11 +88,13 @@ if [[ $DOCKER ]]; then elif [[ $TRAVIS_OS_NAME == osx ]]; then # Disable App Nap defaults write NSGlobalDomain NSAppSleepDisabled -bool YES - curl -LO https://github.com/The-Compiler/homebrew-qt5-webkit/releases/download/v5.6.0-1/pyqt5-5.6.el_capitan.bottle.1.tar.gz - curl -LO https://github.com/The-Compiler/homebrew-qt5-webkit/releases/download/v5.6.1_1-1/qt5-5.6.1-1.yosemite.bottle.1.tar.gz + + curl -LO https://bootstrap.pypa.io/get-pip.py + sudo -H python get-pip.py + brew --version - brew_install python3 {qt5,pyqt5}-*.bottle.1.tar.gz - pip_install pip + brew_install python3 qt5 pyqt5 + pip_install tox pip --version tox --version diff --git a/tests/end2end/data/insert_mode_settings/html/autofocus.html b/tests/end2end/data/insert_mode_settings/html/autofocus.html index db5399252..5b707426f 100644 --- a/tests/end2end/data/insert_mode_settings/html/autofocus.html +++ b/tests/end2end/data/insert_mode_settings/html/autofocus.html @@ -3,8 +3,17 @@ Inputs Autofocus + - + diff --git a/tests/end2end/data/insert_mode_settings/html/input.html b/tests/end2end/data/insert_mode_settings/html/input.html index 2965693d3..0617cae3f 100644 --- a/tests/end2end/data/insert_mode_settings/html/input.html +++ b/tests/end2end/data/insert_mode_settings/html/input.html @@ -3,8 +3,17 @@ Input + - + diff --git a/tests/end2end/data/insert_mode_settings/html/textarea.html b/tests/end2end/data/insert_mode_settings/html/textarea.html index 403809a3a..1ab9d7524 100644 --- a/tests/end2end/data/insert_mode_settings/html/textarea.html +++ b/tests/end2end/data/insert_mode_settings/html/textarea.html @@ -3,8 +3,16 @@ Textarea + - + diff --git a/tests/end2end/features/caret.feature b/tests/end2end/features/caret.feature index fdfb7f6d8..47b00622d 100644 --- a/tests/end2end/features/caret.feature +++ b/tests/end2end/features/caret.feature @@ -3,7 +3,7 @@ Feature: Caret mode Background: Given I open data/caret.html - And I run :tab-only ;; :enter-mode caret + And I run :tab-only ;; enter-mode caret # document diff --git a/tests/end2end/features/completion.feature b/tests/end2end/features/completion.feature index 18f2141d8..4fec2d81b 100644 --- a/tests/end2end/features/completion.feature +++ b/tests/end2end/features/completion.feature @@ -22,3 +22,8 @@ Feature: Command bar completion # Make sure qutebrowser doesn't hang And I run :message-info "Still alive!" Then the message "Still alive!" should be shown + + Scenario: Crash when pasting emoji into the command line (#2007) + Given I open about:blank + When I run :set-cmd-text -s :🌀 + Then no crash should happen diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index 08e6d718a..d0622b9c0 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -182,10 +182,11 @@ Feature: Using hints ### hints -> auto-follow-timeout + @not_osx Scenario: Ignoring key presses after auto-following hints - When I set hints -> auto-follow-timeout to 500 + When I set hints -> auto-follow-timeout to 1000 And I set hints -> mode to number - And I run :bind --force , message-error "This should not happen" + And I run :bind --force , message-error "This error message was triggered via a keybinding which should have been inhibited" And I open data/hints/html/simple.html And I hint with args "all" And I press the key "f" diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature index 48703d940..c60cb0a17 100644 --- a/tests/end2end/features/keyinput.feature +++ b/tests/end2end/features/keyinput.feature @@ -165,7 +165,7 @@ Feature: Keyboard input Then the javascript message "key press: 88" should be logged And the javascript message "key release: 88" should be logged - @no_xvfb @posix + @no_xvfb @posix @qtwebengine_skip Scenario: :fake-key sending key to the website with other window focused When I open data/keyinput/log.html And I set general -> developer-extras to true diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index bb8811292..0727f97cb 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -149,7 +149,7 @@ Feature: Various utility commands. And I run :inspector Then the error "Please enable developer-extras before using the webinspector!" should be shown - @no_xvfb @posix + @no_xvfb @posix @qtwebengine_skip Scenario: Inspector smoke test When I set general -> developer-extras to true And I run :inspector @@ -165,7 +165,7 @@ Feature: Various utility commands. Then the error "Please enable developer-extras before using the webinspector!" should be shown # Different code path as an inspector got created now - @no_xvfb @posix + @no_xvfb @posix @qtwebengine_skip Scenario: Inspector smoke test 2 When I set general -> developer-extras to true And I run :inspector @@ -499,7 +499,7 @@ Feature: Various utility commands. Scenario: Using :debug-log-capacity When I run :debug-log-capacity 100 And I run :message-info oldstuff - And I run :repeat 20 :message-info otherstuff + And I run :repeat 20 message-info otherstuff And I run :message-info newstuff And I open qute:log Then the page should contain the plaintext "newstuff" @@ -549,6 +549,7 @@ Feature: Various utility commands. And I open cookies/set?qute-test=42 without waiting And I wait until cookies is loaded And I open cookies in a new tab + And I set general -> private-browsing to false Then the cookie qute-test should be set to 42 ## https://github.com/The-Compiler/qutebrowser/issues/1742 @@ -557,6 +558,7 @@ Feature: Various utility commands. Scenario: Private browsing is activated in QtWebKit without restart When I set general -> private-browsing to true And I open data/javascript/localstorage.html + And I set general -> private-browsing to false Then the page should contain the plaintext "Local storage status: not working" Scenario: :repeat-command @@ -695,3 +697,63 @@ Feature: Various utility commands. And I wait until the scroll position changed And I run :click-element id link Then the error "Element position is out of view!" should be shown + + ## :command-history-{prev,next} + + Scenario: Calling previous command + When I run :set-cmd-text :message-info blah + And I run :command-accept + And I wait for "blah" in the log + And I run :set-cmd-text : + And I run :command-history-prev + And I run :command-accept + Then the message "blah" should be shown + + Scenario: Browsing through commands + When I run :set-cmd-text :message-info blarg + And I run :command-accept + And I wait for "blarg" in the log + And I run :set-cmd-text : + And I run :command-history-prev + And I run :command-history-prev + And I run :command-history-next + And I run :command-history-next + And I run :command-accept + Then the message "blarg" should be shown + + Scenario: Calling previous command when history is empty + Given I have a fresh instance + When I run :set-cmd-text : + And I run :command-history-prev + And I run :command-accept + Then the error "No command given" should be shown + + Scenario: Calling next command when there's no next command + When I run :set-cmd-text : + And I run :command-history-next + And I run :command-accept + Then the error "No command given" should be shown + + @qtwebengine_todo: private browsing is not implemented yet + Scenario: Calling previous command with private-browsing mode + When I run :set-cmd-text :message-info blah + And I run :command-accept + And I set general -> private-browsing to true + And I run :set-cmd-text :message-error "This should only be shown once" + And I run :command-accept + And I wait for the error "This should only be shown once" + And I run :set-cmd-text : + And I run :command-history-prev + And I run :command-accept + And I set general -> private-browsing to false + Then the message "blah" should be shown + + ## :run-with-count + + Scenario: :run-with-count + When I run :run-with-count 2 scroll down + Then "command called: scroll ['down'] (count=2)" should be logged + + Scenario: :run-with-count with count + When I run :run-with-count 2 scroll down with count 3 + Then "command called: scroll ['down'] (count=6)" should be logged diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature index 234a428ae..af3e43f11 100644 --- a/tests/end2end/features/sessions.feature +++ b/tests/end2end/features/sessions.feature @@ -294,11 +294,13 @@ Feature: Saving and loading sessions Scenario: Deleting internal session with --force When I run :session-save --force _internal And I run :session-delete --force _internal + And I wait for "Deleted session _internal." in the log Then the session _internal should not exist Scenario: Normally deleting a session When I run :session-save deleted_session And I run :session-delete deleted_session + And I wait for "Deleted session deleted_session." in the log Then the session deleted_session should not exist Scenario: Deleting a session which doesn't exist diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 7ae98619c..b138397c5 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -987,13 +987,13 @@ Feature: Tab management Scenario: Using :tab-next after closing last tab (#1448) When I set tabs -> last-close to close And I run :tab-only - And I run :tab-close ;; :tab-next + And I run :tab-close ;; tab-next Then qutebrowser should quit And no crash should happen Scenario: Using :tab-prev after closing last tab (#1448) When I set tabs -> last-close to close And I run :tab-only - And I run :tab-close ;; :tab-prev + And I run :tab-close ;; tab-prev Then qutebrowser should quit And no crash should happen diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index acd6b0f49..46a0d2ff7 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -437,7 +437,8 @@ class QuteProc(testprocess.Process): command = command.replace('\\', r'\\') if count is not None: - command = ':{}:{}'.format(count, command.lstrip(':')) + command = ':run-with-count {} {}'.format(count, + command.lstrip(':')) self.send_ipc([command]) if not invalid: diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py index 791faf9c3..0fa1d7139 100644 --- a/tests/end2end/fixtures/webserver_sub.py +++ b/tests/end2end/fixtures/webserver_sub.py @@ -80,8 +80,11 @@ def redirect_later(): @app.route('/custom/redirect-later-continue') def redirect_later_continue(): """Continue a redirect-later request.""" - _redirect_later_event.set() - return flask.Response(b'Continued redirect.') + if _redirect_later_event is None: + return flask.Response(b'Timed out or no redirect pending.') + else: + _redirect_later_event.set() + return flask.Response(b'Continued redirect.') @app.route('/custom/content-size') diff --git a/tests/end2end/test_insert_mode.py b/tests/end2end/test_insert_mode.py index cd57968ca..6939dae0e 100644 --- a/tests/end2end/test_insert_mode.py +++ b/tests/end2end/test_insert_mode.py @@ -56,6 +56,10 @@ def test_insert_mode(file_name, elem_id, source, input_text, auto_insert, # second time. quteproc.send_cmd(':debug-set-fake-clipboard "{}"'.format(input_text)) quteproc.send_cmd(':insert-text {clipboard}') + else: + raise ValueError("Invalid source {!r}".format(source)) + + quteproc.wait_for_js('contents: {}'.format(input_text)) quteproc.send_cmd(':leave-mode') quteproc.send_cmd(':hint all') diff --git a/tests/unit/browser/test_pdfjs.py b/tests/unit/browser/test_pdfjs.py index ad489bc7a..a33dae5bf 100644 --- a/tests/unit/browser/test_pdfjs.py +++ b/tests/unit/browser/test_pdfjs.py @@ -36,11 +36,11 @@ from qutebrowser.browser import pdfjs 'http://foobar/%22);alert(%22attack!%22);'), ]) def test_generate_pdfjs_script(url, expected): - expected_code = ('PDFJS.verbosity = PDFJS.VERBOSITY_LEVELS.info;\n' - 'PDFView.open("{}");\n'.format(expected)) + expected_open = 'open("{}");'.format(expected) url = QUrl(url) actual = pdfjs._generate_pdfjs_script(url) - assert actual == expected_code + assert expected_open in actual + assert 'PDFView' in actual def test_fix_urls(): diff --git a/tests/unit/browser/webkit/test_mhtml.py b/tests/unit/browser/webkit/test_mhtml.py index 7ea3d999e..3b33857e9 100644 --- a/tests/unit/browser/webkit/test_mhtml.py +++ b/tests/unit/browser/webkit/test_mhtml.py @@ -26,6 +26,15 @@ import pytest mhtml = pytest.importorskip('qutebrowser.browser.webkit.mhtml') +try: + import cssutils +except (ImportError, re.error): + # Catching re.error because cssutils in earlier releases (<= 1.0) is + # broken on Python 3.5 + # See https://bitbucket.org/cthedot/cssutils/issues/52 + cssutils = None + + @pytest.fixture(autouse=True) def patch_uuid(monkeypatch): monkeypatch.setattr("uuid.uuid4", lambda: "UUID") @@ -248,8 +257,7 @@ def test_empty_content_type(checker): @pytest.mark.parametrize('has_cssutils', [ - pytest.mark.skipif(mhtml.cssutils is None, - reason="requires cssutils")(True), + pytest.mark.skipif(cssutils is None, reason="requires cssutils")(True), False, ], ids=['with_cssutils', 'no_cssutils']) @pytest.mark.parametrize('inline, style, expected_urls', [ @@ -267,7 +275,8 @@ def test_empty_content_type(checker): def test_css_url_scanner(monkeypatch, has_cssutils, inline, style, expected_urls): if not has_cssutils: - monkeypatch.setattr('qutebrowser.browser.webkit.mhtml.cssutils', None) + monkeypatch.setattr(mhtml, '_get_css_imports_cssutils', + lambda data, inline=False: None) expected_urls.sort() urls = mhtml._get_css_imports(style, inline=inline) urls.sort() diff --git a/tests/unit/commands/test_runners.py b/tests/unit/commands/test_runners.py index 50fcbde1c..77c9d8f72 100644 --- a/tests/unit/commands/test_runners.py +++ b/tests/unit/commands/test_runners.py @@ -64,15 +64,6 @@ class TestCommandRunner: with pytest.raises(cmdexc.NoSuchCommandError): list(cr.parse_all(command)) - def test_parse_with_count(self): - """Test parsing of commands with a count.""" - cr = runners.CommandRunner(0) - result = cr.parse('20:scroll down') - assert result.cmd.name == 'scroll' - assert result.count == 20 - assert result.args == ['down'] - assert result.cmdline == ['scroll', 'down'] - def test_partial_parsing(self): """Test partial parsing with a runner where it's enabled. diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index 7e9fdbb48..2c02efa44 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -181,6 +181,7 @@ def _set_cmd_prompt(cmd, txt): (':open -- |', None, ''), (':gibberish nonesense |', None, ''), ('/:help|', None, ''), + ('::bind|', usertypes.Completion.command, ':bind'), ]) def test_update_completion(txt, kind, pattern, status_command_stub, completer_obj, completion_widget_stub): diff --git a/tests/unit/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py index 00f8b290d..bb84521d0 100644 --- a/tests/unit/mainwindow/test_tabwidget.py +++ b/tests/unit/mainwindow/test_tabwidget.py @@ -41,6 +41,7 @@ class TestTabWidget: 'position': 0, 'select-on-remove': 1, 'show': 'always', + 'show-favicons': True, 'padding': configtypes.PaddingValues(0, 0, 5, 5), 'indicator-width': 3, 'indicator-padding': configtypes.PaddingValues(2, 2, 0, 4),