Merge branch 'master' of https://github.com/The-Compiler/qutebrowser into pintab
This commit is contained in:
commit
23628cdfbf
@ -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
|
||||
~~~~~~~
|
||||
|
@ -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
|
||||
|
@ -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' ...]]+
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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([
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
105
qutebrowser/keyinput/macros.py
Normal file
105
qutebrowser/keyinput/macros.py
Normal 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)
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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__()
|
||||
|
@ -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():
|
||||
|
Loading…
Reference in New Issue
Block a user