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) the `readability-lxml` python package)
- New `cast` userscript to show a video on a Google Chromecast - New `cast` userscript to show a video on a Google Chromecast
- New `:run-with-count` command which replaces the (undocumented) `:count:command` syntax. - New `:run-with-count` command which replaces the (undocumented) `:count:command` syntax.
- New `:record-macro` (`q`) and `:run-macro` (`@`) commands for keyboard macros.
Changed Changed
~~~~~~~ ~~~~~~~

View File

@ -147,9 +147,9 @@ Contributors, sorted by the number of commits in descending order:
* Daniel Schadt * Daniel Schadt
* Ryan Roden-Corrent * Ryan Roden-Corrent
* Jakub Klinkovský * Jakub Klinkovský
* Jan Verbeek
* Antoni Boucher * Antoni Boucher
* Lamar Pavel * Lamar Pavel
* Jan Verbeek
* Marshall Lochbaum * Marshall Lochbaum
* Bruno Oliveira * Bruno Oliveira
* Alexander Cogneau * 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-load,quickmark-load>>|Load a quickmark.
|<<quickmark-save,quickmark-save>>|Save the current page as a quickmark. |<<quickmark-save,quickmark-save>>|Save the current page as a quickmark.
|<<quit,quit>>|Quit qutebrowser. |<<quit,quit>>|Quit qutebrowser.
|<<record-macro,record-macro>>|Start or stop recording a macro.
|<<reload,reload>>|Reload the current/[count]th tab. |<<reload,reload>>|Reload the current/[count]th tab.
|<<repeat,repeat>>|Repeat a given command. |<<repeat,repeat>>|Repeat a given command.
|<<report,report>>|Report a bug in qutebrowser. |<<report,report>>|Report a bug in qutebrowser.
|<<restart,restart>>|Restart qutebrowser while keeping existing tabs open. |<<restart,restart>>|Restart qutebrowser while keeping existing tabs open.
|<<run-macro,run-macro>>|Run a recorded macro.
|<<save,save>>|Save configs and state. |<<save,save>>|Save configs and state.
|<<search,search>>|Search for a text on the current page. With no text, clear results. |<<search,search>>|Search for a text on the current page. With no text, clear results.
|<<session-delete,session-delete>>|Delete a session. |<<session-delete,session-delete>>|Delete a session.
@ -602,6 +604,15 @@ Save the current page as a quickmark.
=== quit === quit
Quit qutebrowser. 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]]
=== reload === reload
Syntax: +:reload [*--force*]+ Syntax: +:reload [*--force*]+
@ -637,6 +648,18 @@ Report a bug in qutebrowser.
=== restart === restart
Restart qutebrowser while keeping existing tabs open. 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]]
=== save === save
Syntax: +:save ['what' ['what' ...]]+ Syntax: +:save ['what' ['what' ...]]+

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,6 @@ Module attributes:
_HANDLERS: The handlers registered via decorators. _HANDLERS: The handlers registered via decorators.
""" """
import mimetypes
import urllib.parse import urllib.parse
import qutebrowser import qutebrowser
@ -103,7 +102,7 @@ class add_handler: # pylint: disable=invalid-name
url=url.toDisplayString(), url=url.toDisplayString(),
error='{} is not available with this ' error='{} is not available with this '
'backend'.format(url.toDisplayString()), 'backend'.format(url.toDisplayString()),
icon='', qutescheme=True) icon='')
return 'text/html', html return 'text/html', html
@ -238,8 +237,7 @@ def qute_help(url):
"repository, please run scripts/asciidoc2html.py. " "repository, please run scripts/asciidoc2html.py. "
"If you're running a released version this is a bug, please " "If you're running a released version this is a bug, please "
"use :report to report it.", "use :report to report it.",
icon='', icon='')
qutescheme=True)
return 'text/html', html return 'text/html', html
urlpath = url.path() urlpath = url.path()
if not urlpath or urlpath == '/': if not urlpath or urlpath == '/':
@ -255,12 +253,3 @@ def qute_help(url):
else: else:
data = utils.read_file(path) data = utils.read_file(path)
return 'text/html', data 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', html = jinja.render('error.html',
title="Error while reading directory", title="Error while reading directory",
url='file:///{}'.format(path), error=str(e), url='file:///{}'.format(path), error=str(e),
icon='', qutescheme=False) icon='')
return html.encode('UTF-8', errors='xmlcharrefreplace') return html.encode('UTF-8', errors='xmlcharrefreplace')
files = get_file_list(path, all_files, os.path.isfile) 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) title = "Error loading page: {}".format(urlstr)
error_html = jinja.render( error_html = jinja.render(
'error.html', 'error.html',
title=title, url=urlstr, error=error_str, icon='', title=title, url=urlstr, error=error_str, icon='')
qutescheme=False)
errpage.content = error_html.encode('utf-8') errpage.content = error_html.encode('utf-8')
errpage.encoding = 'utf-8' errpage.encoding = 'utf-8'
return True return True

View File

@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config, configexc from qutebrowser.config import config, configexc
from qutebrowser.commands import cmdexc, cmdutils 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 from qutebrowser.misc import split
@ -259,20 +259,34 @@ class CommandRunner(QObject):
text: The text to parse. text: The text to parse.
count: The count to pass to the command. count: The count to pass to the command.
""" """
for result in self.parse_all(text): record_last_command = True
record_macro = True
mode_manager = objreg.get('mode-manager', scope='window', mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id) window=self._win_id)
cur_mode = mode_manager.mode cur_mode = mode_manager.mode
for result in self.parse_all(text):
if result.cmd.no_replace_variables: if result.cmd.no_replace_variables:
args = result.args args = result.args
else: else:
args = replace_variables(self._win_id, result.args) args = replace_variables(self._win_id, result.args)
result.cmd.run(self._win_id, args, count=count) result.cmd.run(self._win_id, args, count=count)
if result.cmdline[0] != 'repeat-command': 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) 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, int)
@pyqtSlot(str) @pyqtSlot(str)
def run_safely(self, text, count=None): def run_safely(self, text, count=None):

View File

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

View File

@ -73,7 +73,7 @@ function searchFor(uri) {
<table> <table>
<tr> <tr>
<td style="width: 10%; vertical-align: top;"> <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>
<td style="padding-left: 40px;"> <td style="padding-left: 40px;">
<h1>Unable to load page</h1> <h1>Unable to load page</h1>

View File

@ -70,10 +70,11 @@ class BaseKeyParser(QObject):
request_leave: Emitted to request leaving a mode. request_leave: Emitted to request leaving a mode.
arg 0: Mode to leave. arg 0: Mode to leave.
arg 1: Reason for leaving. arg 1: Reason for leaving.
arg 2: Ignore the request if we're not in that mode
""" """
keystring_updated = pyqtSignal(str) keystring_updated = pyqtSignal(str)
request_leave = pyqtSignal(usertypes.KeyMode, str) request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
do_log = True do_log = True
passthrough = False 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), warn=False),
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman), KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
KM.caret: modeparsers.CaretKeyParser(win_id, modeman), KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
KM.set_mark: modeparsers.MarkKeyParser(win_id, KM.set_mark, modeman), KM.set_mark: modeparsers.RegisterKeyParser(win_id, KM.set_mark,
KM.jump_mark: modeparsers.MarkKeyParser(win_id, KM.jump_mark, modeman), 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) objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
modeman.destroyed.connect( 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) 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'.""" """Leave the mode 'mode'."""
instance(win_id).leave(mode, reason) instance(win_id).leave(mode, reason, maybe=maybe)
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))
class ModeManager(QObject): class ModeManager(QObject):
@ -270,16 +267,24 @@ class ModeManager(QObject):
raise cmdexc.CommandError("Mode {} does not exist!".format(mode)) raise cmdexc.CommandError("Mode {} does not exist!".format(mode))
self.enter(m, 'command') self.enter(m, 'command')
@pyqtSlot(usertypes.KeyMode, str) @pyqtSlot(usertypes.KeyMode, str, bool)
def leave(self, mode, reason=None): def leave(self, mode, reason=None, maybe=False):
"""Leave a key mode. """Leave a key mode.
Args: Args:
mode: The mode to leave as a usertypes.KeyMode member. mode: The mode to leave as a usertypes.KeyMode member.
reason: Why the mode was left. reason: Why the mode was left.
maybe: If set, ignore the request if we're not in that mode.
""" """
if self.mode != mode: if self.mode != 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)) raise NotInModeError("Not in mode {}!".format(mode))
log.modes.debug("Leaving mode {}{}".format( log.modes.debug("Leaving mode {}{}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason))) mode, '' if reason is None else ' (reason: {})'.format(reason)))
# leaving a mode implies clearing keychain, see # leaving a mode implies clearing keychain, see

View File

@ -23,11 +23,14 @@ Module attributes:
STARTCHARS: Possible chars for starting a commandline input. STARTCHARS: Possible chars for starting a commandline input.
""" """
import traceback
from PyQt5.QtCore import pyqtSlot, Qt from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.commands import cmdexc
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.keyinput import keyparser from qutebrowser.keyinput import keyparser
from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.utils import usertypes, log, message, objreg, utils
STARTCHARS = ":/?" STARTCHARS = ":/?"
@ -264,12 +267,13 @@ class CaretKeyParser(keyparser.CommandKeyParser):
self.read_config('caret') 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: 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): def __init__(self, win_id, mode, parent=None):
@ -278,7 +282,7 @@ class MarkKeyParser(keyparser.BaseKeyParser):
self._mode = mode self._mode = mode
def handle(self, e): 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: Args:
e: the KeyPressEvent from Qt. e: the KeyPressEvent from Qt.
@ -294,23 +298,32 @@ class MarkKeyParser(keyparser.BaseKeyParser):
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id) window=self._win_id)
macro_recorder = objreg.get('macro-recorder')
try:
if self._mode == usertypes.KeyMode.set_mark: if self._mode == usertypes.KeyMode.set_mark:
tabbed_browser.set_mark(key) tabbed_browser.set_mark(key)
elif self._mode == usertypes.KeyMode.jump_mark: elif self._mode == usertypes.KeyMode.jump_mark:
tabbed_browser.jump_mark(key) 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: else:
raise ValueError("{} is not a valid mark mode".format(self._mode)) 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 return True
@pyqtSlot(str) @pyqtSlot(str)
def on_keyconfig_changed(self, mode): def on_keyconfig_changed(self, mode):
"""MarkKeyParser has no config section (no bindable keys).""" """RegisterKeyParser has no config section (no bindable keys)."""
pass pass
def execute(self, cmdstr, _keytype, count=None): def execute(self, cmdstr, _keytype, count=None):
"""Should never be called on MarkKeyParser.""" """Should never be called on RegisterKeyParser."""
assert False assert False

View File

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

View File

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

View File

@ -22,11 +22,13 @@
import os import os
import os.path import os.path
import traceback import traceback
import mimetypes
import base64
import jinja2 import jinja2
import jinja2.exceptions import jinja2.exceptions
from qutebrowser.utils import utils, urlutils, log, qtutils from qutebrowser.utils import utils, urlutils, log
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
@ -64,25 +66,24 @@ def _guess_autoescape(template_name):
return ext in ['html', 'htm', 'xml'] return ext in ['html', 'htm', 'xml']
def resource_url(path, qutescheme=False): def resource_url(path):
"""Load images from a relative path (to qutebrowser). """Load images from a relative path (to qutebrowser).
Arguments: Arguments:
path: The relative path to the image 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: image = utils.resource_filename(path)
url = QUrl() return QUrl.fromLocalFile(image).toString(QUrl.FullyEncoded)
url.setScheme('qute')
url.setHost('resource')
url.setPath('/' + path) def data_url(path):
qtutils.ensure_valid(url) """Get a data: url for the broken qutebrowser logo."""
return url.toString(QUrl.FullyEncoded) data = utils.read_file(path, binary=True)
else: filename = utils.resource_filename(path)
full_path = utils.resource_filename(path) mimetype = mimetypes.guess_type(filename)
return QUrl.fromLocalFile(full_path).toString(QUrl.FullyEncoded) 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): def render(template, **kwargs):
@ -100,3 +101,4 @@ def render(template, **kwargs):
_env = jinja2.Environment(loader=Loader('html'), autoescape=_guess_autoescape) _env = jinja2.Environment(loader=Loader('html'), autoescape=_guess_autoescape)
_env.globals['resource_url'] = resource_url _env.globals['resource_url'] = resource_url
_env.globals['file_url'] = urlutils.file_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 # Key input modes
KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
'insert', 'passthrough', 'caret', 'set_mark', 'insert', 'passthrough', 'caret', 'set_mark',
'jump_mark']) 'jump_mark', 'record_macro', 'run_macro'])
# Available command completions # Available command completions

View File

@ -163,7 +163,10 @@ PERFECT_FILES = [
# 100% coverage because of end2end tests, but no perfect unit tests yet. # 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): class Skipped(Exception):

View File

@ -121,11 +121,11 @@ Feature: Keyboard input
When I open data/keyinput/log.html When I open data/keyinput/log.html
And I set general -> log-javascript-console to info And I set general -> log-javascript-console to info
And I set input -> forward-unbound-keys to all 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>" And I press the key "<F1>"
# q # ,
Then the javascript message "key press: 81" should be logged Then the javascript message "key press: 188" should be logged
And the javascript message "key release: 81" should be logged And the javascript message "key release: 188" should be logged
# <F1> # <F1>
And the javascript message "key press: 112" should be logged And the javascript message "key press: 112" should be logged
And the javascript message "key release: 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 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 And I wait for "got keypress in mode KeyMode.normal - delegating to <qutebrowser.keyinput.modeparsers.NormalKeyParser>" in the log
Then no crash should happen 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.""" """A fake BaseKeyParser which doesn't handle anything."""
request_leave = pyqtSignal(usertypes.KeyMode, str) request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
def __init__(self): def __init__(self):
super().__init__() super().__init__()

View File

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