This commit is contained in:
thuck 2016-11-10 20:20:52 +01:00
commit 23628cdfbf
26 changed files with 348 additions and 110 deletions

View File

@ -49,6 +49,7 @@ Added
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.
- New `:record-macro` (`q`) and `:run-macro` (`@`) commands for keyboard macros.
Changed
~~~~~~~

View File

@ -147,9 +147,9 @@ Contributors, sorted by the number of commits in descending order:
* Daniel Schadt
* Ryan Roden-Corrent
* Jakub Klinkovský
* Jan Verbeek
* Antoni Boucher
* Lamar Pavel
* Jan Verbeek
* Marshall Lochbaum
* Bruno Oliveira
* Alexander Cogneau

View File

@ -59,10 +59,12 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<quickmark-load,quickmark-load>>|Load a quickmark.
|<<quickmark-save,quickmark-save>>|Save the current page as a quickmark.
|<<quit,quit>>|Quit qutebrowser.
|<<record-macro,record-macro>>|Start or stop recording a macro.
|<<reload,reload>>|Reload the current/[count]th tab.
|<<repeat,repeat>>|Repeat a given command.
|<<report,report>>|Report a bug in qutebrowser.
|<<restart,restart>>|Restart qutebrowser while keeping existing tabs open.
|<<run-macro,run-macro>>|Run a recorded macro.
|<<save,save>>|Save configs and state.
|<<search,search>>|Search for a text on the current page. With no text, clear results.
|<<session-delete,session-delete>>|Delete a session.
@ -602,6 +604,15 @@ Save the current page as a quickmark.
=== quit
Quit qutebrowser.
[[record-macro]]
=== record-macro
Syntax: +:record-macro ['register']+
Start or stop recording a macro.
==== positional arguments
* +'register'+: Which register to store the macro in.
[[reload]]
=== reload
Syntax: +:reload [*--force*]+
@ -637,6 +648,18 @@ Report a bug in qutebrowser.
=== restart
Restart qutebrowser while keeping existing tabs open.
[[run-macro]]
=== run-macro
Syntax: +:run-macro ['register']+
Run a recorded macro.
==== positional arguments
* +'register'+: Which macro to run.
==== count
How many times to run the macro.
[[save]]
=== save
Syntax: +:save ['what' ['what' ...]]+

View File

@ -11,7 +11,7 @@ httpbin==0.5.0
hypothesis==3.6.0
itsdangerous==0.24
# Jinja2==2.8
Mako==1.0.5
Mako==1.0.6
# MarkupSafe==0.23
parse==1.6.6
parse-type==0.3.4

View File

@ -49,6 +49,7 @@ from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads)
from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
crashsignal, earlyinit)
@ -157,6 +158,8 @@ def init(args, crash_handler):
QDesktopServices.setUrlHandler('https', open_desktopservices_url)
QDesktopServices.setUrlHandler('qute', open_desktopservices_url)
macros.init()
log.init.debug("Init done!")
crash_handler.raise_crashdlg()

View File

@ -792,8 +792,8 @@ class CommandDispatcher:
message.info("{} {} yanked to {}".format(
len(s), "char" if len(s) == 1 else "chars", target))
if not keep:
modeman.maybe_leave(self._win_id, KeyMode.caret,
"yank selected")
modeman.leave(self._win_id, KeyMode.caret, "yank selected",
maybe=True)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)

View File

@ -50,7 +50,8 @@ class HintingError(Exception):
def on_mode_entered(mode, win_id):
"""Stop hinting when insert mode was entered."""
if mode == usertypes.KeyMode.insert:
modeman.maybe_leave(win_id, usertypes.KeyMode.hint, 'insert mode')
modeman.leave(win_id, usertypes.KeyMode.hint, 'insert mode',
maybe=True)
class HintLabel(QLabel):
@ -859,8 +860,8 @@ class HintManager(QObject):
raise ValueError("No suitable handler found!")
if not self._context.rapid:
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
'followed')
modeman.leave(self._win_id, usertypes.KeyMode.hint, 'followed',
maybe=True)
else:
# Reset filtering
self.filter_hints(None)

View File

@ -151,9 +151,8 @@ class MouseEventFilter(QObject):
else:
log.mouse.debug("Clicked non-editable element!")
if config.get('input', 'auto-leave-insert-mode'):
modeman.maybe_leave(self._tab.win_id,
usertypes.KeyMode.insert,
'click')
modeman.leave(self._tab.win_id, usertypes.KeyMode.insert,
'click', maybe=True)
def _mouserelease_insertmode(self):
"""If we have an insertmode check scheduled, handle it."""
@ -174,9 +173,8 @@ class MouseEventFilter(QObject):
else:
log.mouse.debug("Clicked non-editable element (delayed)!")
if config.get('input', 'auto-leave-insert-mode'):
modeman.maybe_leave(self._tab.win_id,
usertypes.KeyMode.insert,
'click-delayed')
modeman.leave(self._tab.win_id, usertypes.KeyMode.insert,
'click-delayed', maybe=True)
self._tab.elements.find_focused(mouserelease_insertmode_cb)

View File

@ -24,7 +24,6 @@ Module attributes:
_HANDLERS: The handlers registered via decorators.
"""
import mimetypes
import urllib.parse
import qutebrowser
@ -103,7 +102,7 @@ class add_handler: # pylint: disable=invalid-name
url=url.toDisplayString(),
error='{} is not available with this '
'backend'.format(url.toDisplayString()),
icon='', qutescheme=True)
icon='')
return 'text/html', html
@ -238,8 +237,7 @@ def qute_help(url):
"repository, please run scripts/asciidoc2html.py. "
"If you're running a released version this is a bug, please "
"use :report to report it.",
icon='',
qutescheme=True)
icon='')
return 'text/html', html
urlpath = url.path()
if not urlpath or urlpath == '/':
@ -255,12 +253,3 @@ def qute_help(url):
else:
data = utils.read_file(path)
return 'text/html', data
@add_handler('resource')
def qute_resource(url):
"""Serve resources via a qute://resource/... URL."""
data = utils.read_file(url.path(), binary=True)
mimetype, _encoding = mimetypes.guess_type(url.fileName())
assert mimetype is not None, url
return mimetype, data

View File

@ -102,7 +102,7 @@ def dirbrowser_html(path):
html = jinja.render('error.html',
title="Error while reading directory",
url='file:///{}'.format(path), error=str(e),
icon='', qutescheme=False)
icon='')
return html.encode('UTF-8', errors='xmlcharrefreplace')
files = get_file_list(path, all_files, os.path.isfile)

View File

@ -178,8 +178,7 @@ class BrowserPage(QWebPage):
title = "Error loading page: {}".format(urlstr)
error_html = jinja.render(
'error.html',
title=title, url=urlstr, error=error_str, icon='',
qutescheme=False)
title=title, url=urlstr, error=error_str, icon='')
errpage.content = error_html.encode('utf-8')
errpage.encoding = 'utf-8'
return True

View File

@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config, configexc
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, objreg, qtutils, utils
from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split
@ -259,19 +259,33 @@ class CommandRunner(QObject):
text: The text to parse.
count: The count to pass to the command.
"""
for result in self.parse_all(text):
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
cur_mode = mode_manager.mode
record_last_command = True
record_macro = True
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
cur_mode = mode_manager.mode
for result in self.parse_all(text):
if result.cmd.no_replace_variables:
args = result.args
else:
args = replace_variables(self._win_id, result.args)
result.cmd.run(self._win_id, args, count=count)
if result.cmdline[0] != 'repeat-command':
last_command[cur_mode] = (text, count)
if result.cmdline[0] == 'repeat-command':
record_last_command = False
if result.cmdline[0] in ['record-macro', 'run-macro',
'set-cmd-text']:
record_macro = False
if record_last_command:
last_command[cur_mode] = (text, count)
if record_macro and cur_mode == usertypes.KeyMode.normal:
macro_recorder = objreg.get('macro-recorder')
macro_recorder.record_command(text, count)
@pyqtSlot(str, int)
@pyqtSlot(str)

View File

@ -1670,6 +1670,8 @@ KEY_DATA = collections.OrderedDict([
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
('repeat-command', ['.']),
('tab-pin', ['<Ctrl-p>']),
('record-macro', ['q']),
('run-macro', ['@']),
])),
('insert', collections.OrderedDict([

View File

@ -73,13 +73,13 @@ function searchFor(uri) {
<table>
<tr>
<td style="width: 10%; vertical-align: top;">
<img style="width: 100%; display: block; max-width: 256px;" src="{{ resource_url('img/broken_qutebrowser_logo.png', qutescheme) }}" />
<img style="width: 100%; display: block; max-width: 256px;" src="{{ data_url('img/broken_qutebrowser_logo.png') }}" />
</td>
<td style="padding-left: 40px;">
<h1>Unable to load page</h1>
Error while opening {{ url }}: <br>
<p id="error-message-text" style="color: #a31a1a;">{{ error }}</p><br><br>
<form name="bl">
<input type="button" value="Try again" onclick="javascript:tryagain()" />
</form>

View File

@ -70,10 +70,11 @@ class BaseKeyParser(QObject):
request_leave: Emitted to request leaving a mode.
arg 0: Mode to leave.
arg 1: Reason for leaving.
arg 2: Ignore the request if we're not in that mode
"""
keystring_updated = pyqtSignal(str)
request_leave = pyqtSignal(usertypes.KeyMode, str)
request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
do_log = True
passthrough = False

View File

@ -0,0 +1,105 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Jan Verbeek (blyxxyz) <ring@openmailbox.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Keyboard macro system."""
from qutebrowser.commands import cmdexc, cmdutils, runners
from qutebrowser.keyinput import modeman
from qutebrowser.utils import message, objreg, usertypes
class MacroRecorder:
"""An object for recording and running keyboard macros.
Attributes:
_macros: A list of commands for each macro register.
_recording_macro: The register to which a macro is being recorded.
_macro_count: The count passed to run_macro_command for each window.
Stored for use by run_macro, which may be called from
keyinput/modeparsers.py after a key input.
"""
def __init__(self):
self._macros = {}
self._recording_macro = None
self._macro_count = {}
@cmdutils.register(instance='macro-recorder', name='record-macro')
@cmdutils.argument('win_id', win_id=True)
def record_macro_command(self, win_id, register=None):
"""Start or stop recording a macro.
Args:
register: Which register to store the macro in.
"""
if self._recording_macro is None:
if register is None:
mode_manager = modeman.instance(win_id)
mode_manager.enter(usertypes.KeyMode.record_macro,
'record_macro')
else:
self.record_macro(register)
else:
message.info("Macro '{}' recorded.".format(self._recording_macro))
self._recording_macro = None
def record_macro(self, register):
"""Start recording a macro."""
message.info("Recording macro '{}'...".format(register))
self._macros[register] = []
self._recording_macro = register
@cmdutils.register(instance='macro-recorder', name='run-macro')
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('count', count=True)
def run_macro_command(self, win_id, count=1, register=None):
"""Run a recorded macro.
Args:
count: How many times to run the macro.
register: Which macro to run.
"""
self._macro_count[win_id] = count
if register is None:
mode_manager = modeman.instance(win_id)
mode_manager.enter(usertypes.KeyMode.run_macro, 'run_macro')
else:
self.run_macro(win_id, register)
def run_macro(self, win_id, register):
"""Run a recorded macro."""
if register not in self._macros:
raise cmdexc.CommandError(
"No macro recorded in '{}'!".format(register))
commandrunner = runners.CommandRunner(win_id)
for _ in range(self._macro_count[win_id]):
for cmd in self._macros[register]:
commandrunner.run_safely(*cmd)
def record_command(self, text, count):
"""Record a command if a macro is being recorded."""
if self._recording_macro is not None:
self._macros[self._recording_macro].append((text, count))
def init():
"""Initialize the MacroRecorder."""
macro_recorder = MacroRecorder()
objreg.register('macro-recorder', macro_recorder)

View File

@ -78,8 +78,14 @@ def init(win_id, parent):
warn=False),
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
KM.set_mark: modeparsers.MarkKeyParser(win_id, KM.set_mark, modeman),
KM.jump_mark: modeparsers.MarkKeyParser(win_id, KM.jump_mark, modeman),
KM.set_mark: modeparsers.RegisterKeyParser(win_id, KM.set_mark,
modeman),
KM.jump_mark: modeparsers.RegisterKeyParser(win_id, KM.jump_mark,
modeman),
KM.record_macro: modeparsers.RegisterKeyParser(win_id, KM.record_macro,
modeman),
KM.run_macro: modeparsers.RegisterKeyParser(win_id, KM.run_macro,
modeman),
}
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
modeman.destroyed.connect(
@ -100,18 +106,9 @@ def enter(win_id, mode, reason=None, only_if_normal=False):
instance(win_id).enter(mode, reason, only_if_normal)
def leave(win_id, mode, reason=None):
def leave(win_id, mode, reason=None, *, maybe=False):
"""Leave the mode 'mode'."""
instance(win_id).leave(mode, reason)
def maybe_leave(win_id, mode, reason=None):
"""Convenience method to leave 'mode' without exceptions."""
try:
instance(win_id).leave(mode, reason)
except NotInModeError as e:
# This is rather likely to happen, so we only log to debug log.
log.modes.debug("{} (leave reason: {})".format(e, reason))
instance(win_id).leave(mode, reason, maybe=maybe)
class ModeManager(QObject):
@ -270,16 +267,24 @@ class ModeManager(QObject):
raise cmdexc.CommandError("Mode {} does not exist!".format(mode))
self.enter(m, 'command')
@pyqtSlot(usertypes.KeyMode, str)
def leave(self, mode, reason=None):
@pyqtSlot(usertypes.KeyMode, str, bool)
def leave(self, mode, reason=None, maybe=False):
"""Leave a key mode.
Args:
mode: The mode to leave as a usertypes.KeyMode member.
reason: Why the mode was left.
maybe: If set, ignore the request if we're not in that mode.
"""
if self.mode != mode:
raise NotInModeError("Not in mode {}!".format(mode))
if maybe:
log.modes.debug("Ignoring leave request for {} (reason {}) as "
"we're in mode {}".format(
mode, reason, self.mode))
return
else:
raise NotInModeError("Not in mode {}!".format(mode))
log.modes.debug("Leaving mode {}{}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason)))
# leaving a mode implies clearing keychain, see

View File

@ -23,11 +23,14 @@ Module attributes:
STARTCHARS: Possible chars for starting a commandline input.
"""
import traceback
from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.commands import cmdexc
from qutebrowser.config import config
from qutebrowser.keyinput import keyparser
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.utils import usertypes, log, message, objreg, utils
STARTCHARS = ":/?"
@ -264,12 +267,13 @@ class CaretKeyParser(keyparser.CommandKeyParser):
self.read_config('caret')
class MarkKeyParser(keyparser.BaseKeyParser):
class RegisterKeyParser(keyparser.BaseKeyParser):
"""KeyParser for set_mark and jump_mark mode.
"""KeyParser for modes that record a register key.
Attributes:
_mode: Either KeyMode.set_mark or KeyMode.jump_mark.
_mode: One of KeyMode.set_mark, KeyMode.jump_mark, KeyMode.record_macro
and KeyMode.run_macro.
"""
def __init__(self, win_id, mode, parent=None):
@ -278,7 +282,7 @@ class MarkKeyParser(keyparser.BaseKeyParser):
self._mode = mode
def handle(self, e):
"""Override handle to always match the next key and create a mark.
"""Override handle to always match the next key and use the register.
Args:
e: the KeyPressEvent from Qt.
@ -294,23 +298,32 @@ class MarkKeyParser(keyparser.BaseKeyParser):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
macro_recorder = objreg.get('macro-recorder')
if self._mode == usertypes.KeyMode.set_mark:
tabbed_browser.set_mark(key)
elif self._mode == usertypes.KeyMode.jump_mark:
tabbed_browser.jump_mark(key)
else:
raise ValueError("{} is not a valid mark mode".format(self._mode))
try:
if self._mode == usertypes.KeyMode.set_mark:
tabbed_browser.set_mark(key)
elif self._mode == usertypes.KeyMode.jump_mark:
tabbed_browser.jump_mark(key)
elif self._mode == usertypes.KeyMode.record_macro:
macro_recorder.record_macro(key)
elif self._mode == usertypes.KeyMode.run_macro:
macro_recorder.run_macro(self._win_id, key)
else:
raise ValueError(
"{} is not a valid register mode".format(self._mode))
except (cmdexc.CommandMetaError, cmdexc.CommandError) as err:
message.error(str(err), stack=traceback.format_exc())
self.request_leave.emit(self._mode, "valid mark key")
self.request_leave.emit(self._mode, "valid register key", True)
return True
@pyqtSlot(str)
def on_keyconfig_changed(self, mode):
"""MarkKeyParser has no config section (no bindable keys)."""
"""RegisterKeyParser has no config section (no bindable keys)."""
pass
def execute(self, cmdstr, _keytype, count=None):
"""Should never be called on MarkKeyParser."""
"""Should never be called on RegisterKeyParser."""
assert False

View File

@ -314,8 +314,8 @@ class PromptContainer(QWidget):
if not question.interrupted:
# If this question was interrupted, we already connected the signal
question.aborted.connect(
lambda: modeman.maybe_leave(self._win_id, prompt.KEY_MODE,
'aborted'))
lambda: modeman.leave(self._win_id, prompt.KEY_MODE, 'aborted',
maybe=True))
modeman.enter(self._win_id, prompt.KEY_MODE, 'question asked')
self.setSizePolicy(prompt.sizePolicy())
@ -328,7 +328,7 @@ class PromptContainer(QWidget):
@pyqtSlot(usertypes.KeyMode)
def _on_prompt_done(self, key_mode):
"""Leave the prompt mode in this window if a question was answered."""
modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept')
modeman.leave(self._win_id, key_mode, ':prompt-accept', maybe=True)
@pyqtSlot(usertypes.KeyMode)
def _on_global_mode_left(self, mode):
@ -339,7 +339,7 @@ class PromptContainer(QWidget):
"""
if mode not in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]:
return
modeman.maybe_leave(self._win_id, mode, 'left in other window')
modeman.leave(self._win_id, mode, 'left in other window', maybe=True)
item = self._layout.takeAt(0)
if item is not None:
widget = item.widget()

View File

@ -474,10 +474,10 @@ class TabbedBrowser(tabwidget.TabWidget):
@pyqtSlot()
def on_cur_load_started(self):
"""Leave insert/hint mode when loading started."""
modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
'load started')
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
'load started')
modeman.leave(self._win_id, usertypes.KeyMode.insert, 'load started',
maybe=True)
modeman.leave(self._win_id, usertypes.KeyMode.hint, 'load started',
maybe=True)
@pyqtSlot(browsertab.AbstractTab, str)
def on_title_changed(self, tab, text):
@ -568,7 +568,7 @@ class TabbedBrowser(tabwidget.TabWidget):
tab.setFocus()
for mode in [usertypes.KeyMode.hint, usertypes.KeyMode.insert,
usertypes.KeyMode.caret, usertypes.KeyMode.passthrough]:
modeman.maybe_leave(self._win_id, mode, 'tab changed')
modeman.leave(self._win_id, mode, 'tab changed', maybe=True)
if self._now_focused is not None:
objreg.register('last-focused-tab', self._now_focused, update=True,
scope='window', window=self._win_id)

View File

@ -22,11 +22,13 @@
import os
import os.path
import traceback
import mimetypes
import base64
import jinja2
import jinja2.exceptions
from qutebrowser.utils import utils, urlutils, log, qtutils
from qutebrowser.utils import utils, urlutils, log
from PyQt5.QtCore import QUrl
@ -64,25 +66,24 @@ def _guess_autoescape(template_name):
return ext in ['html', 'htm', 'xml']
def resource_url(path, qutescheme=False):
def resource_url(path):
"""Load images from a relative path (to qutebrowser).
Arguments:
path: The relative path to the image
qutescheme: If the logo needs to be served via a qute:// scheme.
This is the case when we want to show an error page from
there.
"""
if qutescheme:
url = QUrl()
url.setScheme('qute')
url.setHost('resource')
url.setPath('/' + path)
qtutils.ensure_valid(url)
return url.toString(QUrl.FullyEncoded)
else:
full_path = utils.resource_filename(path)
return QUrl.fromLocalFile(full_path).toString(QUrl.FullyEncoded)
image = utils.resource_filename(path)
return QUrl.fromLocalFile(image).toString(QUrl.FullyEncoded)
def data_url(path):
"""Get a data: url for the broken qutebrowser logo."""
data = utils.read_file(path, binary=True)
filename = utils.resource_filename(path)
mimetype = mimetypes.guess_type(filename)
assert mimetype is not None, path
b64 = base64.b64encode(data).decode('ascii')
return 'data:{};charset=utf-8;base64,{}'.format(mimetype[0], b64)
def render(template, **kwargs):
@ -100,3 +101,4 @@ def render(template, **kwargs):
_env = jinja2.Environment(loader=Loader('html'), autoescape=_guess_autoescape)
_env.globals['resource_url'] = resource_url
_env.globals['file_url'] = urlutils.file_url
_env.globals['data_url'] = data_url

View File

@ -233,7 +233,7 @@ ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window',
# Key input modes
KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
'insert', 'passthrough', 'caret', 'set_mark',
'jump_mark'])
'jump_mark', 'record_macro', 'run_macro'])
# Available command completions

View File

@ -163,7 +163,10 @@ PERFECT_FILES = [
# 100% coverage because of end2end tests, but no perfect unit tests yet.
WHITELISTED_FILES = ['qutebrowser/browser/webkit/webkitinspector.py']
WHITELISTED_FILES = [
'qutebrowser/browser/webkit/webkitinspector.py',
'qutebrowser/keyinput/macros.py',
]
class Skipped(Exception):

View File

@ -121,11 +121,11 @@ Feature: Keyboard input
When I open data/keyinput/log.html
And I set general -> log-javascript-console to info
And I set input -> forward-unbound-keys to all
And I press the key "q"
And I press the key ","
And I press the key "<F1>"
# q
Then the javascript message "key press: 81" should be logged
And the javascript message "key release: 81" should be logged
# ,
Then the javascript message "key press: 188" should be logged
And the javascript message "key release: 188" should be logged
# <F1>
And the javascript message "key press: 112" should be logged
And the javascript message "key release: 112" should be logged
@ -196,3 +196,62 @@ Feature: Keyboard input
When I run :fake-key -g x
And I wait for "got keypress in mode KeyMode.normal - delegating to <qutebrowser.keyinput.modeparsers.NormalKeyParser>" in the log
Then no crash should happen
# Macros
Scenario: Recording a simple macro
Given I open data/scroll/simple.html
And I run :tab-only
When I run :scroll down with count 6
And I wait until the scroll position changed
And I run :record-macro
And I press the key "a"
And I run :scroll up
And I run :scroll up
And I wait until the scroll position changed
And I run :record-macro
And I run :run-macro with count 2
And I press the key "a"
And I wait until the scroll position changed to 0/0
Then the page should not be scrolled
Scenario: Recording a named macro
Given I open data/scroll/simple.html
And I run :tab-only
When I run :scroll down with count 6
And I wait until the scroll position changed
And I run :record-macro foo
And I run :scroll up
And I run :scroll up
And I wait until the scroll position changed
And I run :record-macro foo
And I run :run-macro foo with count 2
And I wait until the scroll position changed to 0/0
Then the page should not be scrolled
Scenario: Running an invalid macro
Given I open data/scroll/simple.html
And I run :tab-only
When I run :run-macro
And I press the key "b"
Then the error "No macro recorded in 'b'!" should be shown
And no crash should happen
Scenario: Running an invalid named macro
Given I open data/scroll/simple.html
And I run :tab-only
When I run :run-macro bar
Then the error "No macro recorded in 'bar'!" should be shown
And no crash should happen
Scenario: Running a macro with a mode-switching command
When I open data/hints/html/simple.html
And I run :record-macro a
And I run :hint links normal
And I wait for "hints: *" in the log
And I run :leave-mode
And I run :record-macro a
And I run :run-macro
And I press the key "a"
And I wait for "hints: *" in the log
Then no crash should happen

View File

@ -28,7 +28,7 @@ class FakeKeyparser(QObject):
"""A fake BaseKeyParser which doesn't handle anything."""
request_leave = pyqtSignal(usertypes.KeyMode, str)
request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
def __init__(self):
super().__init__()

View File

@ -34,23 +34,42 @@ from qutebrowser.utils import utils, jinja
def patch_read_file(monkeypatch):
"""pytest fixture to patch utils.read_file."""
real_read_file = utils.read_file
real_resource_filename = utils.resource_filename
def _read_file(path):
def _read_file(path, binary=False):
"""A read_file which returns a simple template if the path is right."""
if path == os.path.join('html', 'test.html'):
assert not binary
return """Hello {{var}}"""
elif path == os.path.join('html', 'resource_url.html'):
return """{{ resource_url('utils/testfile', False) }}"""
elif path == os.path.join('html', 'resource_url_qute.html'):
return """{{ resource_url('utils/testfile', True) }}"""
elif path == os.path.join('html', 'test2.html'):
assert not binary
return """{{ resource_url('utils/testfile') }}"""
elif path == os.path.join('html', 'test3.html'):
assert not binary
return """{{ data_url('testfile.txt') }}"""
elif path == 'testfile.txt':
assert binary
return b'foo'
elif path == os.path.join('html', 'undef.html'):
assert not binary
return """{{ does_not_exist() }}"""
elif path == os.path.join('html', 'undef_error.html'):
assert not binary
return real_read_file(path)
else:
raise IOError("Invalid path {}!".format(path))
def _resource_filename(path):
if path == 'utils/testfile':
return real_resource_filename(path)
elif path == 'testfile.txt':
return path
else:
raise IOError("Invalid path {}!".format(path))
monkeypatch.setattr('qutebrowser.utils.jinja.utils.read_file', _read_file)
monkeypatch.setattr('qutebrowser.utils.jinja.utils.resource_filename',
_resource_filename)
def test_simple_template():
@ -61,7 +80,7 @@ def test_simple_template():
def test_resource_url():
"""Test resource_url() which can be used from templates."""
data = jinja.render('resource_url.html')
data = jinja.render('test2.html')
print(data)
url = QUrl(data)
assert url.isValid()
@ -77,12 +96,13 @@ def test_resource_url():
assert f.read().splitlines()[0] == "Hello World!"
def test_resource_url_qutescheme():
"""Test resource_url() which can be used from templates."""
data = jinja.render('resource_url_qute.html')
def test_data_url():
"""Test data_url() which can be used from templates."""
data = jinja.render('test3.html')
print(data)
url = QUrl(data)
assert url == QUrl('qute://resource/utils/testfile')
assert url.isValid()
assert data == 'data:text/plain;charset=utf-8;base64,Zm9v' # 'foo'
def test_not_found():