Merge branch 'macros' of https://github.com/blyxxyz/qutebrowser into blyxxyz-macros

This commit is contained in:
Florian Bruhin 2016-11-10 06:50:21 +01:00
commit 5d30105a53
10 changed files with 233 additions and 28 deletions

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

@ -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

@ -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

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

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(

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")
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

@ -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

@ -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

View File

@ -530,6 +530,45 @@ Feature: Various utility commands.
history:
- url: http://localhost:*/data/hello3.txt
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 run :record-macro
And I press the key "a"
And I run :scroll up
And I run :scroll up
And I run :record-macro
And I run :run-macro with count 2
And I press the key "a"
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 run :record-macro foo
And I run :scroll up
And I run :scroll up
And I run :record-macro foo
And I run :run-macro foo with count 2
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
## Variables
Scenario: {url} as part of an argument