Merge branch 'master' of ssh://lupin/qutebrowser

This commit is contained in:
Florian Bruhin 2014-05-06 18:16:44 +02:00
commit 66fd51958b
17 changed files with 586 additions and 367 deletions

113
TODO
View File

@ -14,34 +14,26 @@ Crashes
- Chosing printer in :print gives us a segfault on Linux, but :printpreview
works...
- Segfault when closing some tab:
QIODevice::read: device not open
QIODevice::read: device not open
QIODevice::read: device not open
Fatal Python error: Segmentation fault
- When following a hint:
Current thread 0x00007ff4ed080700 (most recent call first):
File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 29 in main
File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 33 in <module>
File "/usr/lib/python3.4/runpy.py", line 86 in _run_code
File "/usr/lib/python3.4/runpy.py", line 171 in _run_module_as_main
QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once.
@ e5000c315dd29ae9356e1b33ed041917c637c85b
- When enabling javascript on http://google.com/accounts and reloading:
- Weird TypeError:
OpenType support missing for script 12
QIODevice::read: device not open
QIODevice::read: device not open
Traceback (most recent call last):
File "/home/florian/proj/qutebrowser/git/qutebrowser/app.py", line 567, in command_handler
handler(*args)
File "/home/florian/proj/qutebrowser/git/qutebrowser/widgets/_tabbedbrowser.py", line 216, in tabclose
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
File "/home/florian/proj/qutebrowser/git/qutebrowser/widgets/webview.py", line 215, in shutdown
netman.destroyed.connect(functools.partial(self._on_destroyed, netman))
TypeError: pyqtSignal must be bound to a QObject, not 'NetworkManager'
Then it quits.
When javascript is enabled already, hangs.
With minimal browser, prints error but continues to run.
Also quits on http://de.wikipedia.org/wiki/Hallo-Welt-Programm
It seems this only happens on Windows.
- Closing some tabs and undo all them -> quits and CMD.exe hangs
QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not dequeu request
@ e5000c315dd29ae9356e1b33ed041917c637c85b
- Sometimes self._frame is None in on_mode_left in HintManager
@ -52,6 +44,24 @@ Bugs
====
- All kind of FIXMEs
- F on duckduckgo result page opens in current page
It seems we don't get a linkClicked signal there.
Maybe it does some weird js stuff?
See also: http://qt-project.org/doc/qt-4.8/qwebpage.html#createWindow
Asked here: http://stackoverflow.com/q/23498666/2085149
- Shutdown is still flaky.
Some pointers:
https://code.google.com/p/webscraping/source/browse/webkit.py
Simply does setPage(None) in __del__ of webview.
http://www.tenox.net/out/wrp11-qt.py
does del self._window; del self._view; del self._page
http://pydoc.net/Python/grab/0.4.5/ghost.ghost/
does webview.close(); del self.manager; del self.page; del self.mainframe
Style
=====
@ -80,6 +90,9 @@ Major features
Minor features
==============
- Enable disk caching
QNetworkManager.setCache() and use a QNetworkDiskCache probably
- clear cookies
- keybind/aliases should have completion for commands/arguments
- Hiding scrollbars
- Ctrl+A/X to increase/decrease last number in URL
@ -102,8 +115,15 @@ hints
- filter close hints when it's the same link
- ignore keypresses shortly after link following
Qt Bugs
========
Useful things
=============
http://www.tenox.net/out/wrp11-qt.py
https://code.google.com/p/webscraping/source/browse/webkit.py
https://github.com/jeanphix/Ghost.py/blob/master/ghost/ghost.py
Upstream Bugs
=============
- Printing under windows produced blank pages
https://bugreports.qt-project.org/browse/QTBUG-19571
@ -116,11 +136,54 @@ Qt Bugs
https://bugreports.qt-project.org/browse/QTBUG-38669
- Web inspector is blank unless .hide()/.show() is called.
Asked on SO: http://stackoverflow.com/q/23499159/2085149
- Weird font rendering
https://bugreports.qt-project.org/browse/QTBUG-20973
https://bugreports.qt-project.org/browse/QTBUG-21036
- dead_actute
https://bugs.freedesktop.org/show_bug.cgi?id=69476
- QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once.
https://bugreports.qt-project.org/browse/QTBUG-30298
Probably fixed crashes
======================
- Segfault when closing some tab:
QIODevice::read: device not open
QIODevice::read: device not open
QIODevice::read: device not open
Fatal Python error: Segmentation fault
Current thread 0x00007ff4ed080700 (most recent call first):
File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 29 in main
File "/home/florian/proj/qutebrowser/git/qutebrowser/__main__.py", line 33 in <module>
File "/usr/lib/python3.4/runpy.py", line 86 in _run_code
File "/usr/lib/python3.4/runpy.py", line 171 in _run_module_as_main
@ e5000c315dd29ae9356e1b33ed041917c637c85b
Probably fixed by de7c6a63b48f66e164bf4baa6e892bcbb326d6b9
- When opening a hint with F:
2014-05-06 09:01:25 [DEBUG] [_tabbedbrowser:tabopen:153] Opening PyQt5.QtCore.QUrl('http://ddg.gg/')
Traceback (most recent call last):
File ".\qutebrowser\widgets\_tabbedbrowser.py", line 155, in tabopen
tab = WebView(self)
File ".\qutebrowser\widgets\webview.py", line 83, in __init__
self.page_ = BrowserPage(self)
File ".\qutebrowser\browser\webpage.py", line 44, in __init__
self.setNetworkAccessManager(QApplication.instance().networkmanager)
RuntimeError: wrapped C/C++ object of type NetworkManager has been deleted
Probably fixed by de7c6a63b48f66e164bf4baa6e892bcbb326d6b9
Keybinding stuff (from dwb)
===========================

View File

@ -20,9 +20,10 @@
import os
import sys
import logging
import functools
import subprocess
import configparser
from bdb import BdbQuit
from functools import partial
from signal import signal, SIGINT
from argparse import ArgumentParser
from base64 import b64encode
@ -102,7 +103,11 @@ class QuteBrowser(QApplication):
def __init__(self):
super().__init__(sys.argv)
self._quit_status = {}
self._quit_status = {
'crash': True,
'tabs': False,
'networkmanager': False
}
self._timers = []
self._opened_urls = []
self._shutting_down = False
@ -392,15 +397,22 @@ class QuteBrowser(QApplication):
exc = (exctype, excvalue, tb)
sys.__excepthook__(*exc)
if not issubclass(exctype, Exception):
# probably a KeyboardInterrupt
self._quit_status['crash'] = False
# Give key input back for crash dialog
try:
self.removeEventFilter(self.modeman)
except AttributeError:
# self.modeman could be None
pass
if exctype is BdbQuit or not issubclass(exctype, Exception):
# pdb exit, KeyboardInterrupt, ...
try:
self.shutdown()
return
except Exception:
self.quit()
self._quit_status['crash'] = False
self._quit_status['shutdown'] = False
try:
pages = self._recover_pages()
except Exception:
@ -483,49 +495,55 @@ class QuteBrowser(QApplication):
@pyqtSlot()
@cmdutils.register(instance='', name=['quit', 'q'], nargs=0)
def shutdown(self, do_quit=True):
def shutdown(self):
"""Try to shutdown everything cleanly.
For some reason lastWindowClosing sometimes seem to get emitted twice,
so we make sure we only run once here.
Args:
do_quit: Whether to quit after shutting down.
"""
if self._shutting_down:
return
self._shutting_down = True
logging.debug("Shutting down... (do_quit={})".format(do_quit))
logging.debug("Shutting down...")
# Save config
if self.config.get('general', 'auto-save-config'):
try:
self.config.save()
except AttributeError:
logging.exception("Could not save config.")
# Save command history
try:
self.cmd_history.save()
except AttributeError:
logging.exception("Could not save command history.")
# Save window state
try:
self._save_geometry()
self.stateconfig.save()
except AttributeError:
logging.exception("Could not save window geometry.")
# Save cookies
try:
self.cookiejar.save()
except AttributeError:
logging.exception("Could not save cookies.")
# Shut down tabs
try:
if do_quit:
self.mainwindow.tabs.shutdown_complete.connect(
self.on_tab_shutdown_complete)
else:
self.mainwindow.tabs.shutdown_complete.connect(
functools.partial(self._maybe_quit, 'shutdown'))
self.mainwindow.tabs.shutdown_complete.connect(partial(
self._maybe_quit, 'tabs'))
self.mainwindow.tabs.shutdown()
except AttributeError: # mainwindow or tabs could still be None
logging.exception("No mainwindow/tabs to shut down.")
if do_quit:
self.quit()
self._maybe_quit('tabs')
# Shut down networkmanager
try:
self.networkmanager.abort_requests()
self.networkmanager.destroyed.connect(partial(
self._maybe_quit, 'networkmanager'))
self.networkmanager.deleteLater()
except AttributeError:
logging.exception("No networkmanager to shut down.")
self._maybe_quit('networkmanager')
@pyqtSlot()
def on_tab_shutdown_complete(self):

View File

@ -21,7 +21,7 @@ import logging
import math
from collections import namedtuple
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QPoint
from PyQt5.QtGui import QMouseEvent, QClipboard
from PyQt5.QtWidgets import QApplication
@ -70,6 +70,7 @@ class HintManager(QObject):
"""
HINT_CSS = """
display: {display};
color: {config[colors][hints.fg]};
background: {config[colors][hints.bg]};
font: {config[fonts][hints]};
@ -193,6 +194,24 @@ class HintManager(QObject):
hintstr.insert(0, chars[0])
return ''.join(hintstr)
def _get_hint_css(self, elem, label=None):
"""Get the hint CSS for the element given.
Args:
elem: The QWebElement to get the CSS for.
label: The label QWebElement if display: none should be preserved.
Return:
The CSS to set as a string.
"""
if label is None or label.attribute('hidden') != 'true':
display = 'inline'
else:
display = 'none'
rect = elem.geometry()
return self.HINT_CSS.format(left=rect.x(), top=rect.y(),
config=config.instance(), display=display)
def _draw_label(self, elem, string):
"""Draw a hint label over an element.
@ -203,9 +222,7 @@ class HintManager(QObject):
Return:
The newly created label elment
"""
rect = elem.geometry()
css = self.HINT_CSS.format(left=rect.x(), top=rect.y(),
config=config.instance())
css = self._get_hint_css(elem)
doc = self._frame.documentElement()
# It seems impossible to create an empty QWebElement for which isNull()
# is false so we can work with it.
@ -227,7 +244,13 @@ class HintManager(QObject):
else:
target = self._target
self.set_open_target.emit(Target[target])
point = elem.geometry().topLeft()
# FIXME this is a quick & dirty fix, we should:
# a) Have better heuristics where to click at (e.g. end of input
# fields)
# b) Check border/margin/padding to know where to click
# Hinting failed here for example:
# https://lsf.fh-worms.de/
point = elem.geometry().topLeft() + QPoint(1, 1)
scrollpos = self._frame.scrollPosition()
logging.debug("Clicking on \"{}\" at {}/{} - {}/{}".format(
elem.toPlainText(), point.x(), point.y(), scrollpos.x(),
@ -382,34 +405,48 @@ class HintManager(QObject):
def handle_partial_key(self, keystr):
"""Handle a new partial keypress."""
delete = []
logging.debug("Handling new keystring: '{}'".format(keystr))
for (string, elems) in self._elems.items():
if string.startswith(keystr):
matched = string[:len(keystr)]
rest = string[len(keystr):]
elems.label.setInnerXml('<font color="{}">{}</font>{}'.format(
config.get('colors', 'hints.fg.match'), matched, rest))
if elems.label.attribute('hidden') == 'true':
# hidden element which matches again -> unhide it
elems.label.setAttribute('hidden', 'false')
css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css)
else:
elems.label.removeFromDocument()
delete.append(string)
for key in delete:
del self._elems[key]
# element doesn't match anymore -> hide it
elems.label.setAttribute('hidden', 'true')
css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css)
def filter_hints(self, filterstr):
"""Filter displayed hints according to a text."""
delete = []
for (string, elems) in self._elems.items():
if not elems.elem.toPlainText().lower().startswith(filterstr):
elems.label.removeFromDocument()
delete.append(string)
for key in delete:
del self._elems[key]
if not self._elems:
for elems in self._elems.values():
if elems.elem.toPlainText().lower().startswith(filterstr):
if elems.label.attribute('hidden') == 'true':
# hidden element which matches again -> unhide it
elems.label.setAttribute('hidden', 'false')
css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css)
else:
# element doesn't match anymore -> hide it
elems.label.setAttribute('hidden', 'true')
css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css)
visible = {}
for k, e in self._elems.items():
if e.label.attribute('hidden') != 'true':
visible[k] = e
if not visible:
# Whoops, filtered all hints
modeman.leave('hint')
elif len(self._elems) == 1 and config.get('hints', 'auto-follow'):
elif len(visible) == 1 and config.get('hints', 'auto-follow'):
# unpacking gets us the first (and only) key in the dict.
self.fire(*self._elems)
self.fire(*visible)
def fire(self, keystr, force=False):
"""Fire a completed hint.
@ -461,9 +498,7 @@ class HintManager(QObject):
def on_contents_size_changed(self, _size):
"""Reposition hints if contents size changed."""
for elems in self._elems.values():
rect = elems.elem.geometry()
css = self.HINT_CSS.format(left=rect.x(), top=rect.y(),
config=config.instance())
css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css)
@pyqtSlot(str)
@ -472,9 +507,14 @@ class HintManager(QObject):
if mode != 'hint':
return
for elem in self._elems.values():
elem.label.removeFromDocument()
self._frame.contentsSizeChanged.disconnect(
self.on_contents_size_changed)
if not elem.label.isNull():
elem.label.removeFromDocument()
if self._frame is not None:
# The frame which was focused in start() might not be available
# anymore, since Qt might already have deleted it (e.g. when a new
# page is loaded).
self._frame.contentsSizeChanged.disconnect(
self.on_contents_size_changed)
self._elems = {}
self._to_follow = None
self._target = None

View File

@ -72,7 +72,7 @@ class BrowserPage(QWebPage):
def userAgentForUrl(self, url):
"""Override QWebPage::userAgentForUrl to customize the user agent."""
ua = config.get('network', 'user-agent')
if not ua:
if ua is None:
return super().userAgentForUrl(url)
else:
return ua

View File

@ -155,17 +155,22 @@ class String(BaseType):
Attributes:
minlen: Minimum length (inclusive).
maxlen: Maximum length (inclusive).
forbidden: Forbidden chars in the string.
none: Whether to convert to None for an empty string.
"""
typestr = 'string'
def __init__(self, minlen=None, maxlen=None, forbidden=None):
def __init__(self, minlen=None, maxlen=None, forbidden=None, none=False):
self.minlen = minlen
self.maxlen = maxlen
self.forbidden = forbidden
self.none = none
def transform(self, value):
return value.lower()
if self.none and not value:
return None
return value
def validate(self, value):
if self.forbidden is not None and any(c in value
@ -180,37 +185,17 @@ class String(BaseType):
self.maxlen))
class ShellCommand(String):
class List(BaseType):
"""A shellcommand which is split via shlex.
"""Base class for a (string-)list setting."""
Attributes:
placeholder: If there should be a placeholder.
"""
typestr = 'shell-cmd'
def __init__(self, placeholder=False):
self.placeholder = placeholder
super().__init__()
def validate(self, value):
super().validate(value)
if self.placeholder and '{}' not in value:
raise ValidationError(value, "needs to contain a {}-placeholder.")
typestr = 'string-list'
def transform(self, value):
return shlex.split(value)
return value.split(',')
class HintMode(BaseType):
"""Base class for the hints -> mode setting."""
typestr = 'hint-mode'
valid_values = ValidValues(('number', "Use numeric hints."),
('letter', "Use the chars in hints -> chars."))
def validate(self, value):
pass
class Bool(BaseType):
@ -245,18 +230,24 @@ class Int(BaseType):
Attributes:
minval: Minimum value (inclusive).
maxval: Maximum value (inclusive).
none: Whether to accept empty values as None.
"""
typestr = 'int'
def __init__(self, minval=None, maxval=None):
def __init__(self, minval=None, maxval=None, none=False):
self.minval = minval
self.maxval = maxval
self.none = none
def transform(self, value):
if self.none and not value:
return None
return int(value)
def validate(self, value):
if self.none and not value:
return
try:
intval = int(value)
except ValueError:
@ -269,20 +260,21 @@ class Int(BaseType):
self.maxval))
class NoneInt(BaseType):
class IntList(List):
"""Int or None."""
"""Base class for an int-list setting."""
typestr = 'int-list'
def transform(self, value):
if value == '':
return None
else:
return super().transform(value)
vals = super().transform(value)
return map(int, vals)
def validate(self, value):
if value == '':
return
super().validate(value)
try:
self.transform(value)
except ValueError:
raise ValidationError(value, "must be a list of integers!")
class Float(BaseType):
@ -316,36 +308,6 @@ class Float(BaseType):
self.maxval))
class List(BaseType):
"""Base class for a (string-)list setting."""
typestr = 'string-list'
def transform(self, value):
return value.split(',')
def validate(self, value):
pass
class IntList(List):
"""Base class for an int-list setting."""
typestr = 'int-list'
def transform(self, value):
vals = super().transform(value)
return map(int, vals)
def validate(self, value):
try:
self.transform(value)
except ValueError:
raise ValidationError(value, "must be a list of integers!")
class Perc(BaseType):
"""Percentage.
@ -355,6 +317,8 @@ class Perc(BaseType):
maxval: Maximum value (inclusive).
"""
typestr = 'percentage'
def __init__(self, minval=None, maxval=None):
self.minval = minval
self.maxval = maxval
@ -406,15 +370,6 @@ class PercList(List):
raise ValidationError(value, "must be a list of percentages!")
class ZoomPerc(Perc):
"""A percentage which needs to be in the current zoom percentages."""
def validate(self, value):
super().validate(value)
# FIXME we should validate the percentage is in the list here.
class PercOrInt(BaseType):
"""Percentage or integer.
@ -426,6 +381,8 @@ class PercOrInt(BaseType):
maxint: Maximum value for integer (inclusive).
"""
typestr = 'percentage-or-int'
def __init__(self, minperc=None, maxperc=None, minint=None, maxint=None):
self.minperc = minperc
self.maxperc = maxperc
@ -457,130 +414,6 @@ class PercOrInt(BaseType):
self.maxint))
class WebKitBytes(BaseType):
"""A size with an optional suffix.
Attributes:
maxsize: The maximum size to be used.
Class attributes:
SUFFIXES: A mapping of size suffixes to multiplicators.
"""
SUFFIXES = {
'k': 1024 ** 1,
'm': 1024 ** 2,
'g': 1024 ** 3,
't': 1024 ** 4,
'p': 1024 ** 5,
'e': 1024 ** 6,
'z': 1024 ** 7,
'y': 1024 ** 8,
}
def __init__(self, maxsize=None):
self.maxsize = maxsize
def validate(self, value):
if value == '':
return
try:
val = self.transform(value)
except ValueError:
raise ValidationError(value, "must be a valid integer with "
"optional suffix!")
if self.maxsize is not None and val > self.maxsize:
raise ValidationError(value, "must be {} "
"maximum!".format(self.maxsize))
def transform(self, value):
if value == '':
return None
if any(value.lower().endswith(c) for c in self.SUFFIXES):
suffix = value[-1].lower()
val = value[:-1]
multiplicator = self.SUFFIXES[suffix]
else:
val = value
multiplicator = 1
return int(val) * multiplicator
class WebKitBytesList(List):
"""A size with an optional suffix.
Attributes:
length: The length of the list.
bytestype: The webkit bytes type.
"""
def __init__(self, maxsize=None, length=None):
self.length = length
self.bytestype = WebKitBytes(maxsize)
def transform(self, value):
if value == '':
return None
vals = super().transform(value)
return [self.bytestype.transform(val) for val in vals]
def validate(self, value):
if value == '':
return
vals = super().transform(value)
for val in vals:
self.bytestype.validate(val)
if any(val is None for val in vals):
raise ValidationError(value, "all values need to be set!")
if self.length is not None and len(vals) != self.length:
raise ValidationError(value, "exactly {} values need to be "
"set!".format(self.length))
class Proxy(BaseType):
"""A proxy URL or special value."""
valid_values = ValidValues(('system', "Use the system wide proxy."),
('none', "Don't use any proxy"))
PROXY_TYPES = {
'http': QNetworkProxy.HttpProxy,
'socks': QNetworkProxy.Socks5Proxy,
'socks5': QNetworkProxy.Socks5Proxy,
}
def validate(self, value):
if value in self.valid_values:
return
url = QUrl(value)
if (url.isValid() and not url.isEmpty() and
url.scheme() in self.PROXY_TYPES):
return
raise ValidationError(value, "must be a proxy URL (http://... or "
"socks://...) or system/none!")
def complete(self):
out = []
for val in self.valid_values:
out.append((val, self.valid_values.descriptions[val]))
out.append(('http://', 'HTTP proxy URL'))
out.append(('socks://', 'SOCKS proxy URL'))
return out
def transform(self, value):
if value == 'system':
return None
elif value == 'none':
return QNetworkProxy(QNetworkProxy.NoProxy)
url = QUrl(value)
typ = self.PROXY_TYPES[url.scheme()]
return QNetworkProxy(typ, url.host(), url.port(), url.userName(),
url.password())
class Command(BaseType):
"""Base class for a command value with arguments."""
@ -646,6 +479,225 @@ class Font(BaseType):
pass
class Regex(BaseType):
"""A regular expression."""
typestr = 'regex'
def __init__(self, flags=0):
self.flags = flags
def validate(self, value):
try:
re.compile(value, self.flags)
except RegexError as e:
raise ValidationError(value, "must be a valid regex - " + str(e))
def transform(self, value):
return re.compile(value, self.flags)
class RegexList(List):
"""A list of regexes."""
typestr = 'regex-list'
def __init__(self, flags=0):
self.flags = flags
def transform(self, value):
vals = super().transform(value)
return [re.compile(pattern, self.flags) for pattern in vals]
def validate(self, value):
try:
self.transform(value)
except RegexError as e:
raise ValidationError(value, "must be a list valid regexes - " +
str(e))
class File(BaseType):
"""A file on the local filesystem."""
typestr = 'file'
def validate(self, value):
if not os.path.isfile(value):
raise ValidationError(value, "must be a valid file!")
class WebKitBytes(BaseType):
"""A size with an optional suffix.
Attributes:
maxsize: The maximum size to be used.
Class attributes:
SUFFIXES: A mapping of size suffixes to multiplicators.
"""
SUFFIXES = {
'k': 1024 ** 1,
'm': 1024 ** 2,
'g': 1024 ** 3,
't': 1024 ** 4,
'p': 1024 ** 5,
'e': 1024 ** 6,
'z': 1024 ** 7,
'y': 1024 ** 8,
}
typestr = 'bytes'
def __init__(self, maxsize=None):
self.maxsize = maxsize
def validate(self, value):
if value == '':
return
try:
val = self.transform(value)
except ValueError:
raise ValidationError(value, "must be a valid integer with "
"optional suffix!")
if self.maxsize is not None and val > self.maxsize:
raise ValidationError(value, "must be {} "
"maximum!".format(self.maxsize))
def transform(self, value):
if value == '':
return None
if any(value.lower().endswith(c) for c in self.SUFFIXES):
suffix = value[-1].lower()
val = value[:-1]
multiplicator = self.SUFFIXES[suffix]
else:
val = value
multiplicator = 1
return int(val) * multiplicator
class WebKitBytesList(List):
"""A size with an optional suffix.
Attributes:
length: The length of the list.
bytestype: The webkit bytes type.
"""
typestr = 'bytes-list'
def __init__(self, maxsize=None, length=None):
self.length = length
self.bytestype = WebKitBytes(maxsize)
def transform(self, value):
if value == '':
return None
vals = super().transform(value)
return [self.bytestype.transform(val) for val in vals]
def validate(self, value):
if value == '':
return
vals = super().transform(value)
for val in vals:
self.bytestype.validate(val)
if any(val is None for val in vals):
raise ValidationError(value, "all values need to be set!")
if self.length is not None and len(vals) != self.length:
raise ValidationError(value, "exactly {} values need to be "
"set!".format(self.length))
class ShellCommand(String):
"""A shellcommand which is split via shlex.
Attributes:
placeholder: If there should be a placeholder.
"""
typestr = 'shell-command'
def __init__(self, placeholder=False):
self.placeholder = placeholder
super().__init__()
def validate(self, value):
super().validate(value)
if self.placeholder and '{}' not in value:
raise ValidationError(value, "needs to contain a {}-placeholder.")
def transform(self, value):
return shlex.split(value)
class ZoomPerc(Perc):
"""A percentage which needs to be in the current zoom percentages."""
def validate(self, value):
super().validate(value)
# FIXME we should validate the percentage is in the list here.
class HintMode(BaseType):
"""Base class for the hints -> mode setting."""
valid_values = ValidValues(('number', "Use numeric hints."),
('letter', "Use the chars in hints -> chars."))
class Proxy(BaseType):
"""A proxy URL or special value."""
valid_values = ValidValues(('system', "Use the system wide proxy."),
('none', "Don't use any proxy"))
PROXY_TYPES = {
'http': QNetworkProxy.HttpProxy,
'socks': QNetworkProxy.Socks5Proxy,
'socks5': QNetworkProxy.Socks5Proxy,
}
def validate(self, value):
if value in self.valid_values:
return
url = QUrl(value)
if (url.isValid() and not url.isEmpty() and
url.scheme() in self.PROXY_TYPES):
return
raise ValidationError(value, "must be a proxy URL (http://... or "
"socks://...) or system/none!")
def complete(self):
out = []
for val in self.valid_values:
out.append((val, self.valid_values.descriptions[val]))
out.append(('http://', 'HTTP proxy URL'))
out.append(('socks://', 'SOCKS proxy URL'))
return out
def transform(self, value):
if value == 'system':
return None
elif value == 'none':
return QNetworkProxy(QNetworkProxy.NoProxy)
url = QUrl(value)
typ = self.PROXY_TYPES[url.scheme()]
return QNetworkProxy(typ, url.host(), url.port(), url.userName(),
url.password())
class SearchEngineName(BaseType):
"""A search engine name."""
@ -673,55 +725,19 @@ class KeyBindingName(BaseType):
pass
class Regex(BaseType):
class KeyBinding(Command):
"""A regular expression."""
"""The command of a keybinding."""
def __init__(self, flags=0):
self.flags = flags
def validate(self, value):
try:
re.compile(value, self.flags)
except RegexError as e:
raise ValidationError(value, "must be a valid regex - " + str(e))
def transform(self, value):
return re.compile(value, self.flags)
class RegexList(List):
"""A list of regexes."""
def __init__(self, flags=0):
self.flags = flags
def transform(self, value):
vals = super().transform(value)
return [re.compile(pattern, self.flags) for pattern in vals]
def validate(self, value):
try:
self.transform(value)
except RegexError as e:
raise ValidationError(value, "must be a list valid regexes - " +
str(e))
class File(BaseType):
"""A file on the local filesystem."""
def validate(self, value):
if not os.path.isfile(value):
raise ValidationError(value, "must be a valid file!")
pass
class WebSettingsFile(File):
"""QWebSettings file which also can be none."""
typestr = 'file'
def validate(self, value):
if value == '':
# empty values are okay
@ -791,10 +807,3 @@ class AcceptCookies(String):
valid_values = ValidValues(('default', "Default QtWebKit behaviour"),
('never', "Don't accept cookies at all."))
class KeyBinding(Command):
"""The command of a keybinding."""
pass

View File

@ -271,7 +271,6 @@ class ConfigManager(QObject):
Return:
The value of the option.
"""
logging.debug("getting {} -> {}".format(sectname, optname))
try:
sect = self.sections[sectname]
except KeyError:
@ -285,7 +284,6 @@ class ConfigManager(QObject):
mapping = {key: val.value for key, val in sect.values.items()}
newval = self._interpolation.before_get(self, sectname, optname,
val.value, mapping)
logging.debug("interpolated val: {}".format(newval))
if transformed:
newval = val.typ.transform(newval)
return newval

View File

@ -203,11 +203,11 @@ DATA = OrderedDict([
"Value to send in the DNT header."),
('accept-language',
SettingValue(types.String(), 'en-US,en'),
SettingValue(types.String(none=True), 'en-US,en'),
"Value to send in the accept-language header."),
('user-agent',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"User agent to send. Empty to send the default."),
('accept-cookies',
@ -422,55 +422,55 @@ DATA = OrderedDict([
"User stylesheet to set."),
('css-media-type',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"Set the CSS media type."),
('default-encoding',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"Default encoding to use for websites."),
('font-family-standard',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"Font family for standard fonts."),
('font-family-fixed',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"Font family for fixed fonts."),
('font-family-serif',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"Font family for serif fonts."),
('font-family-sans-serif',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"Font family for sans-serif fonts."),
('font-family-cursive',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"Font family for cursive fonts."),
('font-family-fantasy',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"Font family for fantasy fonts."),
('font-size-minimum',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"The hard minimum font size."),
('font-size-minimum-logical',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"The minimum logical font size that is applied when zooming out."),
('font-size-default',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"The default font size for regular text."),
('font-size-default-fixed',
SettingValue(types.String(), ''),
SettingValue(types.String(none=True), ''),
"The default font size for fixed-pitch text."),
('maximum-pages-in-cache',
SettingValue(types.NoneInt(), ''),
SettingValue(types.Int(none=True), ''),
"The default font size for fixed-pitch text."),
('object-cache-capacities',
@ -485,7 +485,7 @@ DATA = OrderedDict([
('offline-web-application-cache-quota',
SettingValue(types.WebKitBytes(maxsize=INT64_MAX), ''),
"Quota for the offline web application cache>"),
"Quota for the offline web application cache."),
)),
('hints', sect.KeyValue(

View File

@ -150,7 +150,8 @@ class BaseKeyParser(QObject):
"""
logging.debug("Got key: {} / text: '{}'".format(e.key(), e.text()))
txt = e.text().strip()
if not txt:
if not txt or e.key() == Qt.Key_Backspace:
# backspace is counted as text...
logging.debug("Ignoring, no text")
return False
@ -172,9 +173,13 @@ class BaseKeyParser(QObject):
(match, binding) = self._match_key(cmd_input)
if match == self.Match.definitive:
logging.debug("Definitive match for "
"\"{}\".".format(self._keystring))
self._keystring = ''
self.execute(binding, self.Type.chain, count)
elif match == self.Match.ambiguous:
logging.debug("Ambigious match for "
"\"{}\".".format(self._keystring))
self._handle_ambiguous_match(binding, count)
elif match == self.Match.partial:
logging.debug("No match for \"{}\" (added {})".format(

View File

@ -28,9 +28,11 @@ from PyQt5.QtCore import pyqtSignal, Qt
import qutebrowser.utils.message as message
import qutebrowser.config.config as config
from qutebrowser.keyinput.keyparser import CommandKeyParser
from qutebrowser.utils.usertypes import enum
STARTCHARS = ":/?"
LastPress = enum('none', 'filtertext', 'keystring')
class NormalKeyParser(CommandKeyParser):
@ -69,6 +71,7 @@ class HintKeyParser(CommandKeyParser):
Attributes:
_filtertext: The text to filter with.
_last_press: The nature of the last keypress, a LastPress member.
"""
fire_hint = pyqtSignal(str)
@ -77,6 +80,7 @@ class HintKeyParser(CommandKeyParser):
def __init__(self, parent=None):
super().__init__(parent, supports_count=False, supports_chains=True)
self._filtertext = ''
self._last_press = LastPress.none
self.read_config('keybind.hint')
def _handle_special_key(self, e):
@ -92,20 +96,32 @@ class HintKeyParser(CommandKeyParser):
Emit:
filter_hints: Emitted when filter string has changed.
keystring_updated: Emitted when keystring has been changed.
"""
logging.debug("Got special key {} text {}".format(e.key(), e.text()))
if config.get('hints', 'mode') != 'number':
return super()._handle_special_key(e)
elif e.key() == Qt.Key_Backspace:
if self._filtertext:
if e.key() == Qt.Key_Backspace:
logging.debug("Got backspace, mode {}, filtertext \"{}\", "
"keystring \"{}\"".format(
LastPress[self._last_press], self._filtertext,
self._keystring))
if self._last_press == LastPress.filtertext and self._filtertext:
self._filtertext = self._filtertext[:-1]
self.filter_hints.emit(self._filtertext)
return True
return True
elif self._last_press == LastPress.keystring and self._keystring:
self._keystring = self._keystring[:-1]
self.keystring_updated.emit(self._keystring)
return True
else:
return super()._handle_special_key(e)
elif config.get('hints', 'mode') != 'number':
return super()._handle_special_key(e)
elif not e.text():
return super()._handle_special_key(e)
else:
self._filtertext += e.text()
self.filter_hints.emit(self._filtertext)
self._last_press = LastPress.filtertext
return True
def handle(self, e):
@ -120,6 +136,7 @@ class HintKeyParser(CommandKeyParser):
handled = self._handle_single_key(e)
if handled:
self.keystring_updated.emit(self._keystring)
self._last_press = LastPress.keystring
return handled
return self._handle_special_key(e)

View File

@ -72,8 +72,10 @@ class NetworkManager(QNetworkAccessManager):
dnt = '0'.encode('ascii')
req.setRawHeader('DNT'.encode('ascii'), dnt)
req.setRawHeader('X-Do-Not-Track'.encode('ascii'), dnt)
req.setRawHeader('Accept-Language'.encode('ascii'),
config.get('network', 'accept-language'))
accept_language = config.get('network', 'accept-language')
if accept_language is not None:
req.setRawHeader('Accept-Language'.encode('ascii'),
accept_language.encode('ascii'))
reply = super().createRequest(op, req, outgoing_data)
self._requests[id(reply)] = reply
reply.destroyed.connect(lambda obj: self._requests.pop(id(obj)))

View File

@ -137,6 +137,7 @@ class IsUrlNaiveTests(TestCase):
'foo bar',
'localhost test',
'another . test',
'foo',
]
def test_urls(self):

View File

@ -20,6 +20,8 @@
import re
import sys
import shlex
import urllib.request
from urllib.parse import urljoin, urlencode
from functools import reduce
from pkg_resources import resource_string
@ -110,3 +112,24 @@ def shell_escape(s):
# use single quotes, and put single quotes into double quotes
# the string $'b is then quoted as '$'"'"'b'
return "'" + s.replace("'", "'\"'\"'") + "'"
def pastebin(text):
"""Paste the text into a pastebin and return the URL."""
api_url = 'http://paste.the-compiler.org/api/'
data = {
'text': text,
'title': "qutebrowser crash",
'name': "qutebrowser",
}
encoded_data = urlencode(data).encode('utf-8')
create_url = urljoin(api_url, 'create')
headers = {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
}
request = urllib.request.Request(create_url, encoded_data, headers)
response = urllib.request.urlopen(request)
url = response.read().decode('utf-8').rstrip()
if not url.startswith('http'):
raise ValueError("Got unexpected response: {}".format(url))
return url

View File

@ -73,11 +73,14 @@ def _is_url_naive(url):
True if the url really is an URL, False otherwise.
"""
protocols = ['http', 'https']
u = qurl(url)
urlstr = urlstring(url)
if isinstance(url, QUrl):
u = url
else:
u = QUrl.fromUserInput(url)
if u.scheme() in protocols:
# We don't use u here because fromUserInput appends http:// automatically.
if any(urlstr.startswith(proto) for proto in protocols):
return True
elif '.' in u.host():
return True

View File

@ -139,7 +139,8 @@ class TabbedBrowser(TabWidget):
tab.open_tab.connect(self.tabopen)
tab.iconChanged.connect(self.on_icon_changed)
def _tabopen(self, url, background=False):
@pyqtSlot(str, bool)
def tabopen(self, url, background=False):
"""Open a new tab with a given url.
Inner logic for tabopen and backtabopen.
@ -217,7 +218,9 @@ class TabbedBrowser(TabWidget):
return
last_close = config.get('tabbar', 'last-close')
if self.count() > 1:
self._url_stack.append(tab.url())
url = tab.url()
if not url.isEmpty():
self._url_stack.append(url)
self.removeTab(idx)
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
elif last_close == 'quit':
@ -225,15 +228,16 @@ class TabbedBrowser(TabWidget):
elif last_close == 'blank':
tab.openurl('about:blank')
@cmdutils.register(instance='mainwindow.tabs', split=False)
def tabopen(self, url):
@cmdutils.register(instance='mainwindow.tabs', split=False, name='tabopen')
def tabopen_cmd(self, url):
"""Open a new tab with a given url."""
self._tabopen(url, background=False)
self.tabopen(url, background=False)
@cmdutils.register(instance='mainwindow.tabs', split=False)
def backtabopen(self, url):
@cmdutils.register(instance='mainwindow.tabs', split=False,
name='backtabopen')
def backtabopen_cmd(self, url):
"""Open a new tab in background."""
self._tabopen(url, background=True)
self.tabopen(url, background=True)
@cmdutils.register(instance='mainwindow.tabs', hide=True)
def tabopencur(self):

View File

@ -19,11 +19,15 @@
import sys
import traceback
from urllib.error import URLError
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QClipboard
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout)
QVBoxLayout, QHBoxLayout, QApplication)
import qutebrowser.config.config as config
import qutebrowser.utils.misc as utils
from qutebrowser.utils.version import version
@ -39,6 +43,8 @@ class CrashDialog(QDialog):
_hbox: The QHboxLayout containing the buttons
_btn_quit: The quit button
_btn_restore: the restore button
_btn_pastebin: the pastebin button
_url: Pastebin URL QLabel.
"""
def __init__(self, pages, cmdhist, exc):
@ -59,7 +65,7 @@ class CrashDialog(QDialog):
text = ("Argh! qutebrowser crashed unexpectedly.<br/>"
"Please review the info below to remove sensitive data and "
"then submit it to <a href='mailto:crash@qutebrowser.org'>"
"crash@qutebrowser.org</a>.<br/><br/>")
"crash@qutebrowser.org</a> or click 'pastebin'.<br/><br/>")
if pages:
text += ("You can click \"Restore tabs\" to attempt to reopen "
"your open tabs.")
@ -68,15 +74,25 @@ class CrashDialog(QDialog):
self._vbox.addWidget(self._lbl)
self._txt = QTextEdit()
self._txt.setReadOnly(True)
#self._txt.setReadOnly(True)
self._txt.setText(self._crash_info(pages, cmdhist, exc))
self._vbox.addWidget(self._txt)
self._url = QLabel()
self._url.setTextInteractionFlags(Qt.TextSelectableByMouse |
Qt.TextSelectableByKeyboard |
Qt.LinksAccessibleByMouse |
Qt.LinksAccessibleByKeyboard)
self._vbox.addWidget(self._url)
self._hbox = QHBoxLayout()
self._hbox.addStretch()
self._btn_quit = QPushButton()
self._btn_quit.setText("Quit")
self._btn_quit.clicked.connect(self.reject)
self._btn_pastebin = QPushButton()
self._btn_pastebin.setText("Pastebin")
self._btn_pastebin.clicked.connect(self.pastebin)
self._hbox.addWidget(self._btn_quit)
if pages:
self._btn_restore = QPushButton()
@ -84,6 +100,7 @@ class CrashDialog(QDialog):
self._btn_restore.clicked.connect(self.accept)
self._btn_restore.setDefault(True)
self._hbox.addWidget(self._btn_restore)
self._hbox.addWidget(self._btn_pastebin)
self._vbox.addLayout(self._hbox)
@ -116,3 +133,15 @@ class CrashDialog(QDialog):
chunks.append('\n'.join([h, body]))
return '\n\n'.join(chunks)
def pastebin(self):
"""Paste the crash info into the pastebin."""
try:
url = utils.pastebin(self._txt.toPlainText())
except (URLError, ValueError) as e:
self._url.setText('Error while pasting: {}'.format(e))
return
self._btn_pastebin.setEnabled(False)
self._url.setText("URL copied to clipboard: "
"<a href='{}'>{}</a>".format(url, url))
QApplication.clipboard().setText(url, QClipboard.Clipboard)

View File

@ -208,12 +208,6 @@ class WebView(QWebView):
self._destroyed[self] = False
self.destroyed.connect(functools.partial(self._on_destroyed, self))
self.deleteLater()
netman = QApplication.instance().networkmanager
self._destroyed[netman] = False
netman.abort_requests()
netman.destroyed.connect(functools.partial(self._on_destroyed, netman))
netman.deleteLater()
logging.debug("Tab shutdown scheduled")
@pyqtSlot(str)
@ -269,7 +263,9 @@ class WebView(QWebView):
Args:
target: A string to set self._force_open_target to.
"""
self._force_open_target = getattr(Target, target)
t = getattr(Target, target)
logging.debug("Setting force target to {}/{}".format(target, t))
self._force_open_target = t
def paintEvent(self, e):
"""Extend paintEvent to emit a signal if the scroll position changed.
@ -338,14 +334,16 @@ class WebView(QWebView):
self._open_target = self._force_open_target
self._force_open_target = None
logging.debug("Setting force target: {}".format(
self._open_target))
Target[self._open_target]))
elif (e.button() == Qt.MidButton or
e.modifiers() & Qt.ControlModifier):
if config.get('general', 'background-tabs'):
self._open_target = Target.bgtab
else:
self._open_target = Target.tab
logging.debug("Setting target: {}".format(self._open_target))
logging.debug("Middle click, setting target: {}".format(
Target[self._open_target]))
else:
self._open_target = Target.normal
logging.debug("Normal click, setting normal target")
return super().mousePressEvent(e)

View File

@ -29,6 +29,7 @@ import sys
import subprocess
import os
import os.path
import unittest
from collections import OrderedDict
try:
@ -108,6 +109,13 @@ def check_pep257(args=None):
print()
def check_unittest():
print("====== unittest ======")
suite = unittest.TestLoader().discover('.')
result = unittest.TextTestRunner().run(suite)
print()
status['unittest'] = result.wasSuccessful()
def check_line():
"""Run _check_file over a filetree."""
print("====== line ======")
@ -198,6 +206,7 @@ def _get_args(checker):
if do_check_257:
check_pep257(_get_args('pep257'))
check_unittest()
for checker in ['pylint', 'flake8', 'pyroma']:
# FIXME what the hell is the flake8 exit status?
run(checker, _get_args(checker))