Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
293e322905 | ||
|
|
354bd5d606 | ||
|
|
8e619fa74e | ||
|
|
5b944fb272 | ||
|
|
36d2fc4b92 | ||
|
|
26f4acb10a | ||
|
|
991277e9de | ||
|
|
133e959ecc | ||
|
|
be1630f7d0 | ||
|
|
c57bf8701e | ||
|
|
ea2ae94cd0 | ||
|
|
356eb7e5e7 | ||
|
|
bdd2afa1a2 | ||
|
|
e02ff26d0e | ||
|
|
128fb2826a | ||
|
|
3521ee16e4 | ||
|
|
af5cb36591 | ||
|
|
8aaae5b78c | ||
|
|
ced87b163f | ||
|
|
c5459abb65 | ||
|
|
83b7c0dd6f | ||
|
|
93fff9a69c | ||
|
|
b2247fa406 | ||
|
|
fafba9af3f |
@@ -14,6 +14,31 @@ This project adheres to http://semver.org/[Semantic Versioning].
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v0.6.2
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Fixed crash when using `:tab-{prev,next,focus}` right after closing the last
|
||||
tab with `last-close` set to `close`.
|
||||
- Fixed crash when doing `:undo` in a new instance with `tabs -> last-close` set
|
||||
to `default-page`.
|
||||
- Fixed crash when starting with --cachedir=""
|
||||
- Fixed crash in some circumstances when using dictionary hints
|
||||
- Fixed various crashes related to PyQt 5.6
|
||||
|
||||
v0.6.1
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~~
|
||||
|
||||
- Fixed broken cheatsheet image which was missing from package
|
||||
- Fixed occasional crash when switching/disconnecting monitors
|
||||
- Fixed crash when downloading non-ascii files with a broken locale (`LC_ALL=C`)
|
||||
- Added workaround for a Qt/PyQt bug which is too weird to describe here
|
||||
|
||||
v0.6.0
|
||||
------
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
recursive-include qutebrowser *.py
|
||||
recursive-include qutebrowser/html *.html
|
||||
recursive-include qutebrowser/img *.svg *.png
|
||||
recursive-include qutebrowser/test *.py
|
||||
recursive-include qutebrowser/javascript *.js
|
||||
graft qutebrowser/html
|
||||
graft qutebrowser/3rdparty
|
||||
graft icons
|
||||
graft doc/img
|
||||
|
||||
@@ -8,7 +8,7 @@ time, use the `:help` command.
|
||||
What to do now
|
||||
--------------
|
||||
|
||||
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
* View the link:http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
to make yourself familiar with the key bindings: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* Run `:adblock-update` to download adblock lists and activate adblocking.
|
||||
|
||||
@@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (0, 6, 0)
|
||||
__version_info__ = (0, 6, 2)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."
|
||||
|
||||
|
||||
@@ -244,12 +244,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
|
||||
cwd: The cwd to use for fuzzy_url.
|
||||
target_arg: Command line argument received by a running instance via
|
||||
ipc. If the --target argument was not specified, target_arg
|
||||
will be an empty string instead of None. This behavior is
|
||||
caused by the PyQt signal
|
||||
``got_args = pyqtSignal(list, str, str)``
|
||||
used in the misc.ipc.IPCServer class. PyQt converts the
|
||||
None value into a null QString and then back to an empty
|
||||
python string
|
||||
will be an empty string.
|
||||
"""
|
||||
if via_ipc and not args:
|
||||
win_id = mainwindow.get_window(via_ipc, force_window=True)
|
||||
@@ -275,6 +270,8 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
log.init.debug("Startup URL {}".format(cmd))
|
||||
if not cwd: # could also be an empty string due to the PyQt signal
|
||||
cwd = None
|
||||
try:
|
||||
url = urlutils.fuzzy_url(cmd, cwd, relative=True)
|
||||
except urlutils.InvalidUrlError as e:
|
||||
@@ -342,6 +339,7 @@ def _save_version():
|
||||
state_config['general']['version'] = qutebrowser.__version__
|
||||
|
||||
|
||||
@pyqtSlot('QWidget*', 'QWidget*')
|
||||
def on_focus_changed(_old, new):
|
||||
"""Register currently focused main window in the object registry."""
|
||||
if new is None:
|
||||
|
||||
@@ -32,16 +32,23 @@ class DiskCache(QNetworkDiskCache):
|
||||
|
||||
"""Disk cache which sets correct cache dir and size.
|
||||
|
||||
If the cache is deactivated via the command line argument --cachedir="",
|
||||
both attributes _cache_dir and _http_cache_dir are set to None.
|
||||
|
||||
Attributes:
|
||||
_activated: Whether the cache should be used.
|
||||
_cache_dir: The base directory for cache files (standarddir.cache())
|
||||
_http_cache_dir: the HTTP subfolder in _cache_dir.
|
||||
_cache_dir: The base directory for cache files (standarddir.cache()) or
|
||||
None.
|
||||
_http_cache_dir: the HTTP subfolder in _cache_dir or None.
|
||||
"""
|
||||
|
||||
def __init__(self, cache_dir, parent=None):
|
||||
super().__init__(parent)
|
||||
self._cache_dir = cache_dir
|
||||
self._http_cache_dir = os.path.join(cache_dir, 'http')
|
||||
if cache_dir is None:
|
||||
self._http_cache_dir = None
|
||||
else:
|
||||
self._http_cache_dir = os.path.join(cache_dir, 'http')
|
||||
self._maybe_activate()
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
|
||||
@@ -774,6 +774,10 @@ class CommandDispatcher:
|
||||
Args:
|
||||
count: How many tabs to switch back.
|
||||
"""
|
||||
if self._count() == 0:
|
||||
# Running :tab-prev after last tab was closed
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/1448
|
||||
return
|
||||
newidx = self._current_index() - count
|
||||
if newidx >= 0:
|
||||
self._set_current_index(newidx)
|
||||
@@ -790,6 +794,10 @@ class CommandDispatcher:
|
||||
Args:
|
||||
count: How many tabs to switch forward.
|
||||
"""
|
||||
if self._count() == 0:
|
||||
# Running :tab-next after last tab was closed
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/1448
|
||||
return
|
||||
newidx = self._current_index() + count
|
||||
if newidx < self._count():
|
||||
self._set_current_index(newidx)
|
||||
|
||||
@@ -104,6 +104,11 @@ def create_full_filename(basename, filename):
|
||||
Return:
|
||||
The full absolute path, or None if filename creation was not possible.
|
||||
"""
|
||||
# Remove chars which can't be encoded in the filename encoding.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/427
|
||||
encoding = sys.getfilesystemencoding()
|
||||
filename = utils.force_encoding(filename, encoding)
|
||||
basename = utils.force_encoding(basename, encoding)
|
||||
if os.path.isabs(filename) and os.path.isdir(filename):
|
||||
# We got an absolute directory from the user, so we save it under
|
||||
# the default filename in that directory.
|
||||
@@ -226,7 +231,7 @@ class DownloadItemStats(QObject):
|
||||
else:
|
||||
return remaining_bytes / avg
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
@pyqtSlot('qint64', 'qint64')
|
||||
def on_download_progress(self, bytes_done, bytes_total):
|
||||
"""Update local variables when the download progress changed.
|
||||
|
||||
@@ -523,10 +528,6 @@ class DownloadItem(QObject):
|
||||
"existing: {}, fileobj {}".format(
|
||||
filename, self._filename, self.fileobj))
|
||||
filename = os.path.expanduser(filename)
|
||||
# Remove chars which can't be encoded in the filename encoding.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/427
|
||||
encoding = sys.getfilesystemencoding()
|
||||
filename = utils.force_encoding(filename, encoding)
|
||||
self._filename = create_full_filename(self.basename, filename)
|
||||
if self._filename is None:
|
||||
# We only got a filename (without directory) or a relative path
|
||||
@@ -649,7 +650,7 @@ class DownloadItem(QObject):
|
||||
except OSError as e:
|
||||
self._die(e.strerror)
|
||||
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot('QNetworkReply::NetworkError')
|
||||
def on_reply_error(self, code):
|
||||
"""Handle QNetworkReply errors."""
|
||||
if code == QNetworkReply.OperationCanceledError:
|
||||
|
||||
@@ -997,11 +997,14 @@ class WordHinter:
|
||||
def __init__(self):
|
||||
# will be initialized on first use.
|
||||
self.words = set()
|
||||
self.dictionary = None
|
||||
|
||||
def ensure_initialized(self):
|
||||
"""Generate the used words if yet uninialized."""
|
||||
if not self.words:
|
||||
dictionary = config.get("hints", "dictionary")
|
||||
dictionary = config.get("hints", "dictionary")
|
||||
if not self.words or self.dictionary != dictionary:
|
||||
self.words.clear()
|
||||
self.dictionary = dictionary
|
||||
try:
|
||||
with open(dictionary, encoding="UTF-8") as wordfile:
|
||||
alphabet = set(string.ascii_lowercase)
|
||||
@@ -1061,12 +1064,17 @@ class WordHinter:
|
||||
return any(hint.startswith(e) or e.startswith(hint)
|
||||
for e in existing)
|
||||
|
||||
def new_hint_for(self, elem, existing):
|
||||
def filter_prefixes(self, hints, existing):
|
||||
return (h for h in hints if not self.any_prefix(h, existing))
|
||||
|
||||
def new_hint_for(self, elem, existing, fallback):
|
||||
"""Return a hint for elem, not conflicting with the existing."""
|
||||
new = self.tag_words_to_hints(self.extract_tag_words(elem))
|
||||
no_prefixes = (h for h in new if not self.any_prefix(h, existing))
|
||||
new_no_prefixes = self.filter_prefixes(new, existing)
|
||||
fallback_no_prefixes = self.filter_prefixes(fallback, existing)
|
||||
# either the first good, or None
|
||||
return next(no_prefixes, None)
|
||||
return (next(new_no_prefixes, None) or
|
||||
next(fallback_no_prefixes, None))
|
||||
|
||||
def hint(self, elems):
|
||||
"""Produce hint labels based on the html tags.
|
||||
@@ -1086,7 +1094,9 @@ class WordHinter:
|
||||
used_hints = set()
|
||||
words = iter(self.words)
|
||||
for elem in elems:
|
||||
hint = self.new_hint_for(elem, used_hints) or next(words)
|
||||
hint = self.new_hint_for(elem, used_hints, words)
|
||||
if not hint:
|
||||
raise WordHintingError("Not enough words in the dictionary.")
|
||||
used_hints.add(hint)
|
||||
hints.append(hint)
|
||||
return hints
|
||||
|
||||
@@ -253,7 +253,7 @@ class NetworkManager(QNetworkAccessManager):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@pyqtSlot('QNetworkReply', 'QAuthenticator')
|
||||
@pyqtSlot('QNetworkReply*', 'QAuthenticator*')
|
||||
def on_authentication_required(self, reply, authenticator):
|
||||
"""Called when a website needs authentication."""
|
||||
user, password = None, None
|
||||
@@ -286,7 +286,7 @@ class NetworkManager(QNetworkAccessManager):
|
||||
authenticator.setUser(user)
|
||||
authenticator.setPassword(password)
|
||||
|
||||
@pyqtSlot('QNetworkProxy', 'QAuthenticator')
|
||||
@pyqtSlot('QNetworkProxy', 'QAuthenticator*')
|
||||
def on_proxy_authentication_required(self, proxy, authenticator):
|
||||
"""Called when a proxy needs authentication."""
|
||||
proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port())
|
||||
|
||||
@@ -288,7 +288,7 @@ class BrowserPage(QWebPage):
|
||||
window=self._win_id)
|
||||
download_manager.get_request(req, page=self)
|
||||
|
||||
@pyqtSlot('QNetworkReply')
|
||||
@pyqtSlot('QNetworkReply*')
|
||||
def on_unsupported_content(self, reply):
|
||||
"""Handle an unsupportedContent signal.
|
||||
|
||||
@@ -334,7 +334,7 @@ class BrowserPage(QWebPage):
|
||||
else:
|
||||
self.error_occurred = False
|
||||
|
||||
@pyqtSlot('QWebFrame', 'QWebPage::Feature')
|
||||
@pyqtSlot('QWebFrame*', 'QWebPage::Feature')
|
||||
def on_feature_permission_requested(self, frame, feature):
|
||||
"""Ask the user for approval for geolocation/notifications."""
|
||||
options = {
|
||||
@@ -439,7 +439,7 @@ class BrowserPage(QWebPage):
|
||||
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
|
||||
frame.setScrollPosition(data['scroll-pos'])
|
||||
|
||||
@pyqtSlot(str)
|
||||
@pyqtSlot(usertypes.ClickTarget)
|
||||
def on_start_hinting(self, hint_target):
|
||||
"""Emitted before a hinting-click takes place.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QApplication, QStyleFactory
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
|
||||
from PyQt5.QtWebKitWidgets import QWebView, QWebPage, QWebFrame
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
@@ -352,9 +352,14 @@ class WebView(QWebView):
|
||||
frame = self.page().mainFrame()
|
||||
frame.javaScriptWindowObjectCleared.connect(self.add_js_bridge)
|
||||
|
||||
@pyqtSlot()
|
||||
def add_js_bridge(self):
|
||||
"""Add the javascript bridge for qute:... pages."""
|
||||
frame = self.sender()
|
||||
if not isinstance(frame, QWebFrame):
|
||||
log.webview.error("Got non-QWebFrame in add_js_bridge")
|
||||
return
|
||||
|
||||
if frame.url().scheme() == 'qute':
|
||||
bridge = objreg.get('js-bridge')
|
||||
frame.addToJavaScriptWindowObject('qute', bridge)
|
||||
|
||||
@@ -249,6 +249,7 @@ class CommandRunner(QObject):
|
||||
result.cmd.run(self._win_id, args)
|
||||
|
||||
@pyqtSlot(str, int)
|
||||
@pyqtSlot(str)
|
||||
def run_safely(self, text, count=None):
|
||||
"""Run a command and display exceptions in the statusbar."""
|
||||
try:
|
||||
|
||||
@@ -175,9 +175,6 @@ class TabCompletionModel(base.BaseCompletionModel):
|
||||
objreg.get("app").new_window.connect(self.on_new_window)
|
||||
self.rebuild()
|
||||
|
||||
# slot argument should be mainwindow.MainWindow but can't import
|
||||
# that at module level because of import loops.
|
||||
@pyqtSlot(object)
|
||||
def on_new_window(self, window):
|
||||
"""Add hooks to new windows."""
|
||||
window.tabbed_browser.new_tab.connect(self.on_new_tab)
|
||||
|
||||
@@ -95,6 +95,7 @@ class change_filter: # pylint: disable=invalid-name
|
||||
"""
|
||||
if self._function:
|
||||
@pyqtSlot(str, str)
|
||||
@pyqtSlot()
|
||||
@functools.wraps(func)
|
||||
def wrapper(sectname=None, optname=None):
|
||||
if sectname is None and optname is None:
|
||||
@@ -108,6 +109,7 @@ class change_filter: # pylint: disable=invalid-name
|
||||
return func()
|
||||
else:
|
||||
@pyqtSlot(str, str)
|
||||
@pyqtSlot()
|
||||
@functools.wraps(func)
|
||||
def wrapper(wrapper_self, sectname=None, optname=None):
|
||||
if sectname is None and optname is None:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
||||
from qutebrowser.mainwindow.statusbar import textbase
|
||||
from qutebrowser.browser import webview
|
||||
|
||||
|
||||
class Percentage(textbase.TextBase):
|
||||
@@ -48,7 +49,7 @@ class Percentage(textbase.TextBase):
|
||||
else:
|
||||
self.setText('[{:2}%]'.format(y))
|
||||
|
||||
@pyqtSlot(object)
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Update scroll position when tab changed."""
|
||||
self.set_perc(*tab.scroll_pos)
|
||||
|
||||
@@ -59,7 +59,7 @@ class Progress(QProgressBar):
|
||||
self.setValue(0)
|
||||
self.show()
|
||||
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Set the correct value when the current tab changed."""
|
||||
if self is None: # pragma: no branch
|
||||
|
||||
@@ -24,6 +24,7 @@ from PyQt5.QtCore import pyqtSlot
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.mainwindow.statusbar import textbase
|
||||
from qutebrowser.utils import usertypes, log, objreg
|
||||
from qutebrowser.browser import webview
|
||||
|
||||
|
||||
class Text(textbase.TextBase):
|
||||
@@ -98,7 +99,7 @@ class Text(textbase.TextBase):
|
||||
"""Clear jstext when page loading started."""
|
||||
self._jstext = ''
|
||||
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Set the correct jstext when the current tab changed."""
|
||||
self._jstext = tab.statusbar_message
|
||||
|
||||
@@ -158,7 +158,7 @@ class UrlText(textbase.TextBase):
|
||||
self._hover_url = None
|
||||
self._update_url()
|
||||
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Update URL if the tab changed."""
|
||||
self._hover_url = None
|
||||
|
||||
@@ -305,6 +305,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
"""Undo removing of a tab."""
|
||||
# Remove unused tab which may be created after the last tab is closed
|
||||
last_close = config.get('tabs', 'last-close')
|
||||
use_current_tab = False
|
||||
if last_close in ['blank', 'startpage', 'default-page']:
|
||||
only_one_tab_open = self.count() == 1
|
||||
no_history = self.widget(0).history().count() == 1
|
||||
@@ -317,12 +318,17 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
last_close_urlstr = urls[last_close].toString().rstrip('/')
|
||||
first_tab_urlstr = first_tab_url.toString().rstrip('/')
|
||||
last_close_url_used = first_tab_urlstr == last_close_urlstr
|
||||
|
||||
if only_one_tab_open and no_history and last_close_url_used:
|
||||
self.removeTab(0)
|
||||
use_current_tab = (only_one_tab_open and no_history and
|
||||
last_close_url_used)
|
||||
|
||||
url, history_data = self._undo_stack.pop()
|
||||
newtab = self.tabopen(url, background=False)
|
||||
|
||||
if use_current_tab:
|
||||
self.openurl(url, newtab=False)
|
||||
newtab = self.widget(0)
|
||||
else:
|
||||
newtab = self.tabopen(url, background=False)
|
||||
|
||||
qtutils.deserialize(history_data, newtab.history())
|
||||
|
||||
@pyqtSlot('QUrl', bool)
|
||||
|
||||
@@ -221,7 +221,7 @@ class IPCServer(QObject):
|
||||
# This means we only use setSocketOption on Windows...
|
||||
os.chmod(self._server.fullServerName(), 0o700)
|
||||
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot('QLocalSocket::LocalSocketError')
|
||||
def on_error(self, err):
|
||||
"""Raise SocketError on fatal errors."""
|
||||
if self._socket is None:
|
||||
@@ -288,6 +288,55 @@ class IPCServer(QObject):
|
||||
self._socket.error.connect(self.on_error)
|
||||
self._socket.disconnectFromServer()
|
||||
|
||||
def _handle_data(self, data):
|
||||
"""Handle data (as bytes) we got from on_ready_ready_read."""
|
||||
try:
|
||||
decoded = data.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
log.ipc.error("invalid utf-8: {}".format(
|
||||
binascii.hexlify(data)))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
log.ipc.debug("Processing: {}".format(decoded))
|
||||
try:
|
||||
json_data = json.loads(decoded)
|
||||
except ValueError:
|
||||
log.ipc.error("invalid json: {}".format(decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
for name in ('args', 'target_arg'):
|
||||
if name not in json_data:
|
||||
log.ipc.error("Missing {}: {}".format(name, decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
try:
|
||||
protocol_version = int(json_data['protocol_version'])
|
||||
except (KeyError, ValueError):
|
||||
log.ipc.error("invalid version: {}".format(decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
if protocol_version != PROTOCOL_VERSION:
|
||||
log.ipc.error("incompatible version: expected {}, got {}".format(
|
||||
PROTOCOL_VERSION, protocol_version))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
args = json_data['args']
|
||||
|
||||
target_arg = json_data['target_arg']
|
||||
if target_arg is None:
|
||||
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-April/037375.html
|
||||
target_arg = ''
|
||||
|
||||
cwd = json_data.get('cwd', '')
|
||||
assert cwd is not None
|
||||
|
||||
self.got_args.emit(args, target_arg, cwd)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_ready_read(self):
|
||||
"""Read json data from the client."""
|
||||
@@ -302,46 +351,7 @@ class IPCServer(QObject):
|
||||
self.got_raw.emit(data)
|
||||
log.ipc.debug("Read from socket 0x{:x}: {}".format(
|
||||
id(self._socket), data))
|
||||
|
||||
try:
|
||||
decoded = data.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
log.ipc.error("invalid utf-8: {}".format(
|
||||
binascii.hexlify(data)))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
log.ipc.debug("Processing: {}".format(decoded))
|
||||
try:
|
||||
json_data = json.loads(decoded)
|
||||
except ValueError:
|
||||
log.ipc.error("invalid json: {}".format(decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
for name in ('args', 'target_arg'):
|
||||
if name not in json_data:
|
||||
log.ipc.error("Missing {}: {}".format(name,
|
||||
decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
try:
|
||||
protocol_version = int(json_data['protocol_version'])
|
||||
except (KeyError, ValueError):
|
||||
log.ipc.error("invalid version: {}".format(decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
if protocol_version != PROTOCOL_VERSION:
|
||||
log.ipc.error("incompatible version: expected {}, "
|
||||
"got {}".format(
|
||||
PROTOCOL_VERSION, protocol_version))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
cwd = json_data.get('cwd', None)
|
||||
self.got_args.emit(json_data['args'], json_data['target_arg'], cwd)
|
||||
self._handle_data(data)
|
||||
self._timer.start()
|
||||
|
||||
@pyqtSlot()
|
||||
|
||||
@@ -241,7 +241,9 @@ class SessionManager(QObject):
|
||||
log.sessions.debug("Saving session {} to {}...".format(name, path))
|
||||
if last_window:
|
||||
data = self._last_window_session
|
||||
assert data is not None
|
||||
if data is None:
|
||||
log.sessions.error("last_window_session is None while saving!")
|
||||
return
|
||||
else:
|
||||
data = self._save_all()
|
||||
log.sessions.vdebug("Saving data: {}".format(data))
|
||||
|
||||
@@ -25,7 +25,7 @@ import os.path
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication, QStandardPaths
|
||||
|
||||
from qutebrowser.utils import log, qtutils
|
||||
from qutebrowser.utils import log, qtutils, debug
|
||||
|
||||
|
||||
# The argparse namespace passed to init()
|
||||
@@ -124,6 +124,8 @@ def _writable_location(typ):
|
||||
"""Wrapper around QStandardPaths.writableLocation."""
|
||||
with qtutils.unset_organization():
|
||||
path = QStandardPaths.writableLocation(typ)
|
||||
typ_str = debug.qenum_key(QStandardPaths, typ)
|
||||
log.misc.debug("writable location for {}: {}".format(typ_str, path))
|
||||
if not path:
|
||||
raise ValueError("QStandardPaths returned an empty value!")
|
||||
# Qt seems to use '/' as path separator even on Windows...
|
||||
|
||||
@@ -91,7 +91,7 @@ def smoke_test(executable):
|
||||
def build_windows():
|
||||
"""Build windows executables/setups."""
|
||||
utils.print_title("Updating 3rdparty content")
|
||||
update_3rdparty.main()
|
||||
update_3rdparty.update_pdfjs()
|
||||
|
||||
utils.print_title("Building Windows binaries")
|
||||
parts = str(sys.version_info.major), str(sys.version_info.minor)
|
||||
|
||||
@@ -87,7 +87,8 @@ def get_build_exe_options(skip_html=False):
|
||||
'includes': [],
|
||||
'excludes': ['tkinter'],
|
||||
'packages': ['pygments', 'pkg_resources._vendor.packaging',
|
||||
'pkg_resources._vendor.pyparsing'],
|
||||
'pkg_resources._vendor.pyparsing',
|
||||
'pkg_resources._vendor.six'],
|
||||
}
|
||||
|
||||
|
||||
|
||||
BIN
tests/integration/data/downloads/ä-issue908.bin
Normal file
BIN
tests/integration/data/downloads/ä-issue908.bin
Normal file
Binary file not shown.
1
tests/integration/data/hinting.txt
Normal file
1
tests/integration/data/hinting.txt
Normal file
@@ -0,0 +1 @@
|
||||
hinting
|
||||
32
tests/integration/data/hints/issue1393.html
Normal file
32
tests/integration/data/hints/issue1393.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Let's Hint some words</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Word hints</h1>
|
||||
|
||||
<h2>Smart hints</h2>
|
||||
<p>In qutebrowser, urls can not only be hinted with letters and
|
||||
numbers, but also with <a href="../words.txt">words</a>. When there is
|
||||
a sensible url text available, qutebrowser will even use that
|
||||
text to create a <a href="../smart.txt">smart</a> hint.</p>
|
||||
|
||||
<h2>Filled hints</h2>
|
||||
<p>When no smart hints are available, because the hint text is
|
||||
<a href="../l33t.txt">too</a> short or <a href="../l33t.txt">l33t</a> to
|
||||
use, words from a dictionary will be used.</p>
|
||||
|
||||
<h2>Hint conflicts</h2>
|
||||
<p>Of course, hints have to be unique. For instance, all hints
|
||||
below should get a different hint, whether they're smart or
|
||||
not:</p>
|
||||
<ul>
|
||||
<li><a href="../hinting.txt">hinting</a> should be a smart hint</li>
|
||||
<li><a href="../l33t.txt">word</a> is a prefix of words</li>
|
||||
<li><a href="../l33t.txt">3</a> is too 1337</li>
|
||||
<li><a href="../l33t.txt">4</a> is too 1337</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
1
tests/integration/data/l33t.txt
Normal file
1
tests/integration/data/l33t.txt
Normal file
@@ -0,0 +1 @@
|
||||
l33t
|
||||
1
tests/integration/data/smart.txt
Normal file
1
tests/integration/data/smart.txt
Normal file
@@ -0,0 +1 @@
|
||||
smart
|
||||
1
tests/integration/data/words.txt
Normal file
1
tests/integration/data/words.txt
Normal file
@@ -0,0 +1 @@
|
||||
words
|
||||
@@ -607,6 +607,16 @@ Feature: Tab management
|
||||
Then the following tabs should be open:
|
||||
- data/hello.txt (active)
|
||||
|
||||
Scenario: Double-undo with single tab on last-close default page
|
||||
Given I have a fresh instance
|
||||
When I open about:blank
|
||||
And I set tabs -> last-close to default-page
|
||||
And I set general -> default-page to about:blank
|
||||
And I run :undo
|
||||
Then the error "Nothing to undo!" should be shown
|
||||
And the following tabs should be open:
|
||||
- about:blank (active)
|
||||
|
||||
# last-close
|
||||
|
||||
Scenario: last-close = blank
|
||||
@@ -822,3 +832,17 @@ Feature: Tab management
|
||||
When I open data/title.html
|
||||
And I run :buffer "1/2/3"
|
||||
Then the error "No matching tab for: 1/2/3" should be shown
|
||||
|
||||
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
|
||||
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
|
||||
Then qutebrowser should quit
|
||||
And no crash should happen
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
@@ -25,6 +26,7 @@ import os.path
|
||||
import yaml
|
||||
import pytest
|
||||
import bs4
|
||||
import textwrap
|
||||
|
||||
|
||||
def collect_tests():
|
||||
@@ -54,3 +56,35 @@ def test_hints(test_name, quteproc):
|
||||
quteproc.wait_for(message='hints: a', category='hints')
|
||||
quteproc.send_cmd(':follow-hint a')
|
||||
quteproc.wait_for_load_finished('data/' + parsed['target'])
|
||||
|
||||
|
||||
def test_word_hints_issue1393(quteproc, tmpdir):
|
||||
dict_file = tmpdir / 'dict'
|
||||
dict_file.write(textwrap.dedent("""
|
||||
alph
|
||||
beta
|
||||
gamm
|
||||
delt
|
||||
epsi
|
||||
"""))
|
||||
targets = [
|
||||
('words', 'words.txt'),
|
||||
('smart', 'smart.txt'),
|
||||
('hinting', 'hinting.txt'),
|
||||
('alph', 'l33t.txt'),
|
||||
('beta', 'l33t.txt'),
|
||||
('gamm', 'l33t.txt'),
|
||||
('delt', 'l33t.txt'),
|
||||
('epsi', 'l33t.txt'),
|
||||
]
|
||||
|
||||
quteproc.set_setting('hints', 'mode', 'word')
|
||||
quteproc.set_setting('hints', 'dictionary', str(dict_file))
|
||||
|
||||
for hint, target in targets:
|
||||
quteproc.open_path('data/hints/issue1393.html')
|
||||
quteproc.wait_for_load_finished('data/hints/issue1393.html')
|
||||
quteproc.send_cmd(':hint')
|
||||
quteproc.wait_for(message='hints: *', category='hints')
|
||||
quteproc.send_cmd(':follow-hint {}'.format(hint))
|
||||
quteproc.wait_for_load_finished('data/{}'.format(target))
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Test starting qutebrowser with various commandline arguments."""
|
||||
"""Test starting qutebrowser with special arguments/environments."""
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -51,3 +51,23 @@ def test_no_config(tmpdir, quteproc_new):
|
||||
quteproc_new.start(args, env=env)
|
||||
quteproc_new.send_cmd(':quit')
|
||||
quteproc_new.wait_for_quit()
|
||||
|
||||
|
||||
@pytest.mark.linux
|
||||
def test_ascii_locale(httpbin, tmpdir, quteproc_new):
|
||||
"""Test downloads with LC_ALL=C set.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/issues/908
|
||||
"""
|
||||
args = ['--debug', '--no-err-windows', '--temp-basedir', 'about:blank']
|
||||
quteproc_new.start(args, env={'LC_ALL': 'C'})
|
||||
quteproc_new.set_setting('storage', 'download-directory', str(tmpdir))
|
||||
quteproc_new.set_setting('storage', 'prompt-download-directory', 'false')
|
||||
url = 'http://localhost:{port}/data/downloads/ä-issue908.bin'.format(
|
||||
port=httpbin.port)
|
||||
quteproc_new.send_cmd(':download {}'.format(url))
|
||||
quteproc_new.send_cmd(':quit')
|
||||
quteproc_new.wait_for_quit()
|
||||
|
||||
assert len(tmpdir.listdir()) == 1
|
||||
assert (tmpdir / '?-issue908.bin').exists()
|
||||
@@ -321,7 +321,7 @@ class TestDefaultConfig:
|
||||
If it did change, place a new qutebrowser-vx.y.z.conf in old_configs
|
||||
and then increment the version.
|
||||
"""
|
||||
assert qutebrowser.__version__ == '0.6.0'
|
||||
assert qutebrowser.__version__ == '0.6.2'
|
||||
|
||||
@pytest.mark.parametrize('filename',
|
||||
os.listdir(os.path.join(os.path.dirname(__file__), 'old_configs')),
|
||||
|
||||
@@ -447,10 +447,14 @@ class TestSave:
|
||||
sess_man.save(str(session_path))
|
||||
assert 'session' not in state_config['general']
|
||||
|
||||
def test_last_window_session_none(self, sess_man, tmpdir):
|
||||
def test_last_window_session_none(self, caplog, sess_man, tmpdir):
|
||||
session_path = tmpdir / 'foo.yml'
|
||||
with pytest.raises(AssertionError):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
sess_man.save(str(session_path), last_window=True)
|
||||
|
||||
assert len(caplog.records) == 1
|
||||
msg = "last_window_session is None while saving!"
|
||||
assert caplog.records[0].msg == msg
|
||||
assert not session_path.exists()
|
||||
|
||||
def test_last_window_session(self, sess_man, tmpdir):
|
||||
|
||||
Reference in New Issue
Block a user