From 7aaaadac1a1710c5cc84f16c77d9a43f3cee664f Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Thu, 6 Oct 2016 22:24:04 +0200 Subject: [PATCH 1/8] Add keyboard macros --- doc/help/commands.asciidoc | 29 +++++++++++++++++++++ qutebrowser/commands/runners.py | 17 ++++++++++--- qutebrowser/config/configdata.py | 2 ++ qutebrowser/misc/utilcmds.py | 39 +++++++++++++++++++++++++++++ tests/end2end/features/misc.feature | 22 ++++++++++++++++ 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 8cc5184f8..0eee3d918 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -59,10 +59,12 @@ It is possible to run or bind multiple commands by separating them with `;;`. |<>|Load a quickmark. |<>|Save the current page as a quickmark. |<>|Quit qutebrowser. +|<>|Start or stop recording a macro. |<>|Reload the current/[count]th tab. |<>|Repeat a given command. |<>|Report a bug in qutebrowser. |<>|Restart qutebrowser while keeping existing tabs open. +|<>|Run a recorded macro. |<>|Save configs and state. |<>|Search for a text on the current page. With no text, clear results. |<>|Delete a session. @@ -602,6 +604,18 @@ Save the current page as a quickmark. === quit Quit qutebrowser. +[[record-macro]] +=== record-macro +Syntax: +:record-macro ['name']+ + +Start or stop recording a macro. + +==== positional arguments +* +'name'+: Which name to give the macro. + +==== note +* This command does not split arguments after the last argument and handles quotes literally. + [[reload]] === reload Syntax: +:reload [*--force*]+ @@ -637,6 +651,21 @@ Report a bug in qutebrowser. === restart Restart qutebrowser while keeping existing tabs open. +[[run-macro]] +=== run-macro +Syntax: +:run-macro ['name']+ + +Run a recorded macro. + +==== positional arguments +* +'name'+: Which macro to run. + +==== count +How many times to run the macro. + +==== note +* This command does not split arguments after the last argument and handles quotes literally. + [[save]] === save Syntax: +:save ['what' ['what' ...]]+ diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 3d74fc266..e83ca7dbb 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -27,13 +27,15 @@ 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 ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline', 'count']) last_command = {} +macro = {} +recording_macro = None def _current_url(tabbed_browser): @@ -301,10 +303,17 @@ class CommandRunner(QObject): else: result.cmd.run(self._win_id, args) + parsed_command = (self._parse_count(text)[1], + count if count is not None else result.count) + if result.cmdline[0] != 'repeat-command': - last_command[cur_mode] = ( - self._parse_count(text)[1], - count if count is not None else result.count) + last_command[cur_mode] = parsed_command + + if (recording_macro is not None and + cur_mode == usertypes.KeyMode.normal and + result.cmdline[0] not in ['record-macro', 'run-macro', + 'set-cmd-text']): + macro[recording_macro].append(parsed_command) @pyqtSlot(str, int) @pyqtSlot(str) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 3b62f3183..672382ae9 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1634,6 +1634,8 @@ KEY_DATA = collections.OrderedDict([ ('follow-selected', RETURN_KEYS), ('follow-selected -t', ['', '']), ('repeat-command', ['.']), + ('record-macro', ['q']), + ('run-macro', ['@']), ])), ('insert', collections.OrderedDict([ diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 2447afd53..743ba169c 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -234,6 +234,45 @@ def repeat_command(win_id, count=None): commandrunner.run(cmd[0], count if count is not None else cmd[1]) +@cmdutils.register(maxsplit=0) +@cmdutils.argument('name') +def record_macro(name=""): + """Start or stop recording a macro. + + Args: + name: Which name to give the macro. + """ + if runners.recording_macro is None: + message.info("Defining macro...") + runners.macro[name] = [] + runners.recording_macro = name + elif runners.recording_macro == name: + message.info("Macro defined.") + runners.recording_macro = None + else: + raise cmdexc.CommandError( + "Already recording macro '{}'".format(runners.recording_macro)) + + +@cmdutils.register(maxsplit=0) +@cmdutils.argument('win_id', win_id=True) +@cmdutils.argument('count', count=True) +@cmdutils.argument('name') +def run_macro(win_id, count=1, name=""): + """Run a recorded macro. + + Args: + count: How many times to run the macro. + name: Which macro to run. + """ + if name not in runners.macro: + raise cmdexc.CommandError("No macro defined!") + commandrunner = runners.CommandRunner(win_id) + for _ in range(count): + for cmd in runners.macro[name]: + commandrunner.run(*cmd) + + @cmdutils.register(debug=True, name='debug-log-capacity') def log_capacity(capacity: int): """Change the number of log lines to be stored in RAM. diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index c24f0ea9f..62254f866 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -622,6 +622,28 @@ 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 5 + And I run :record-macro + And I run :scroll up + And I run :scroll up + And I run :record-macro + And I run :run-macro with count 2 + 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 5 + 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 + ## Variables Scenario: {url} as part of an argument From 33ff0ba715961454da244fd356023a5dae527cc9 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Sat, 8 Oct 2016 21:17:47 +0200 Subject: [PATCH 2/8] Interactively get macro register --- doc/help/commands.asciidoc | 14 ++----- qutebrowser/commands/runners.py | 2 + qutebrowser/keyinput/modeman.py | 6 ++- qutebrowser/keyinput/modeparsers.py | 22 ++++++---- qutebrowser/misc/utilcmds.py | 55 ++++++++++++++++--------- qutebrowser/utils/usertypes.py | 2 +- tests/end2end/features/keyinput.feature | 8 ++-- tests/end2end/features/misc.feature | 6 ++- 8 files changed, 68 insertions(+), 47 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 0eee3d918..5d7cdaf87 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -606,15 +606,12 @@ Quit qutebrowser. [[record-macro]] === record-macro -Syntax: +:record-macro ['name']+ +Syntax: +:record-macro ['register']+ Start or stop recording a macro. ==== positional arguments -* +'name'+: Which name to give the macro. - -==== note -* This command does not split arguments after the last argument and handles quotes literally. +* +'register'+: Which register to store the macro in. [[reload]] === reload @@ -653,19 +650,16 @@ Restart qutebrowser while keeping existing tabs open. [[run-macro]] === run-macro -Syntax: +:run-macro ['name']+ +Syntax: +:run-macro ['register']+ Run a recorded macro. ==== positional arguments -* +'name'+: Which macro to run. +* +'register'+: Which macro to run. ==== count How many times to run the macro. -==== note -* This command does not split arguments after the last argument and handles quotes literally. - [[save]] === save Syntax: +:save ['what' ['what' ...]]+ diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index e83ca7dbb..35464cb9f 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -34,8 +34,10 @@ from qutebrowser.misc import split ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline', 'count']) last_command = {} + macro = {} recording_macro = None +macro_count = {} def _current_url(tabbed_browser): diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 71a83b9a3..e66e9711c 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -78,8 +78,10 @@ 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( diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index aa788ddc3..c27f4fc73 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -27,6 +27,7 @@ from PyQt5.QtCore import pyqtSlot, Qt from qutebrowser.config import config from qutebrowser.keyinput import keyparser +from qutebrowser.misc import utilcmds from qutebrowser.utils import usertypes, log, objreg, utils @@ -264,12 +265,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 +280,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. @@ -299,18 +301,22 @@ class MarkKeyParser(keyparser.BaseKeyParser): tabbed_browser.set_mark(key) elif self._mode == usertypes.KeyMode.jump_mark: tabbed_browser.jump_mark(key) + elif self._mode == usertypes.KeyMode.record_macro: + utilcmds.record_macro(key) + elif self._mode == usertypes.KeyMode.run_macro: + utilcmds.run_macro(self._win_id, key) else: - raise ValueError("{} is not a valid mark mode".format(self._mode)) + raise ValueError("{} is not a valid register key".format(self._mode)) - 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 diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 743ba169c..cb984d73d 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -234,42 +234,57 @@ def repeat_command(win_id, count=None): commandrunner.run(cmd[0], count if count is not None else cmd[1]) -@cmdutils.register(maxsplit=0) -@cmdutils.argument('name') -def record_macro(name=""): +@cmdutils.register(instance='mode-manager', name='record-macro', scope='window') +@cmdutils.argument('register') +def record_macro_command(self, register=None): """Start or stop recording a macro. Args: - name: Which name to give the macro. + register: Which register to store the macro in. """ if runners.recording_macro is None: - message.info("Defining macro...") - runners.macro[name] = [] - runners.recording_macro = name - elif runners.recording_macro == name: - message.info("Macro defined.") - runners.recording_macro = None + if register is None: + self.enter(usertypes.KeyMode.record_macro, 'record_macro') + else: + record_macro(register) else: - raise cmdexc.CommandError( - "Already recording macro '{}'".format(runners.recording_macro)) + message.info("Macro recorded.") + runners.recording_macro = None -@cmdutils.register(maxsplit=0) +def record_macro(register): + """Start recording a macro.""" + message.info("Recording macro...") + runners.macro[register] = [] + runners.recording_macro = register + + +@cmdutils.register(instance='mode-manager', name='run-macro', scope='window') @cmdutils.argument('win_id', win_id=True) @cmdutils.argument('count', count=True) -@cmdutils.argument('name') -def run_macro(win_id, count=1, name=""): +@cmdutils.argument('register') +def run_macro_command(self, win_id, count=1, register=None): """Run a recorded macro. Args: count: How many times to run the macro. - name: Which macro to run. + register: Which macro to run. """ - if name not in runners.macro: - raise cmdexc.CommandError("No macro defined!") + runners.macro_count[win_id] = count + if register is None: + self.enter(usertypes.KeyMode.run_macro, 'run_macro') + else: + run_macro(win_id, register) + + +def run_macro(win_id, register): + """Run a recorded macro.""" + if register not in runners.macro: + raise cmdexc.CommandError( + "No macro recorded in '{}'!".format(register)) commandrunner = runners.CommandRunner(win_id) - for _ in range(count): - for cmd in runners.macro[name]: + for _ in range(runners.macro_count[win_id]): + for cmd in runners.macro[register]: commandrunner.run(*cmd) diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 6dd3d9674..39d7e4209 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -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 diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature index e02b33d71..48703d940 100644 --- a/tests/end2end/features/keyinput.feature +++ b/tests/end2end/features/keyinput.feature @@ -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 "" - # 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 # And the javascript message "key press: 112" should be logged And the javascript message "key release: 112" should be logged diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 62254f866..bb8811292 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -625,18 +625,20 @@ Feature: Various utility commands. Scenario: Recording a simple macro Given I open data/scroll/simple.html And I run :tab-only - When I run :scroll down with count 5 + 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 5 + When I run :scroll down with count 6 And I run :record-macro foo And I run :scroll up And I run :scroll up From 87899cb6b32bef1d6160cd9dc3067973f19513dd Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Sat, 8 Oct 2016 22:00:26 +0200 Subject: [PATCH 3/8] Fix long lines --- qutebrowser/keyinput/modeman.py | 12 ++++++++---- qutebrowser/keyinput/modeparsers.py | 3 ++- qutebrowser/misc/utilcmds.py | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index e66e9711c..d09fb20f2 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -78,10 +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.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), + 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( diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index c27f4fc73..ba94e4696 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -306,7 +306,8 @@ class RegisterKeyParser(keyparser.BaseKeyParser): elif self._mode == usertypes.KeyMode.run_macro: utilcmds.run_macro(self._win_id, key) else: - raise ValueError("{} is not a valid register key".format(self._mode)) + raise ValueError( + "{} is not a valid register key".format(self._mode)) self.request_leave.emit(self._mode, "valid register key") diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index cb984d73d..010d9f33b 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -234,7 +234,8 @@ def repeat_command(win_id, count=None): commandrunner.run(cmd[0], count if count is not None else cmd[1]) -@cmdutils.register(instance='mode-manager', name='record-macro', scope='window') +@cmdutils.register(instance='mode-manager', name='record-macro', + scope='window') @cmdutils.argument('register') def record_macro_command(self, register=None): """Start or stop recording a macro. From f2b05a03951d41dd19130660a3a22c9fe739db73 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Thu, 20 Oct 2016 02:50:00 +0200 Subject: [PATCH 4/8] Move keyboard macro system to MacroRecorder object --- qutebrowser/app.py | 3 + qutebrowser/commands/runners.py | 34 +++++----- qutebrowser/keyinput/macros.py | 99 +++++++++++++++++++++++++++++ qutebrowser/keyinput/modeparsers.py | 6 +- qutebrowser/misc/utilcmds.py | 55 ---------------- 5 files changed, 123 insertions(+), 74 deletions(-) create mode 100644 qutebrowser/keyinput/macros.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 68397bbd3..9c812cff6 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -49,6 +49,7 @@ from qutebrowser.browser import urlmarks, adblock, history, browsertab from qutebrowser.browser.webkit import cookies, cache, downloads from qutebrowser.browser.webkit.network import (webkitqutescheme, proxy, networkmanager) +from qutebrowser.keyinput import macros from qutebrowser.mainwindow import mainwindow 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() diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 5d068422e..6399d9d9f 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -34,10 +34,6 @@ from qutebrowser.misc import split ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline']) last_command = {} -macro = {} -recording_macro = None -macro_count = {} - def _current_url(tabbed_browser): """Convenience method to get the current url.""" @@ -263,27 +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) - parsed_command = (text, count) + if result.cmdline[0] == 'repeat-command': + record_last_command = False - if result.cmdline[0] != 'repeat-command': - last_command[cur_mode] = parsed_command + if result.cmdline[0] in ['record-macro', 'run-macro', + 'set-cmd-text']: + record_macro = False - if (recording_macro is not None and - cur_mode == usertypes.KeyMode.normal and - result.cmdline[0] not in ['record-macro', 'run-macro', - 'set-cmd-text']): - macro[recording_macro].append(parsed_command) + 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(text, count) @pyqtSlot(str, int) @pyqtSlot(str) diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py new file mode 100644 index 000000000..48c1f0a1d --- /dev/null +++ b/qutebrowser/keyinput/macros.py @@ -0,0 +1,99 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 Jan Verbeek (blyxxyz) +# +# 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 . + +"""Keyboard macro system.""" + +from qutebrowser.commands import cmdexc, cmdutils, runners +from qutebrowser.utils import message, objreg, usertypes + + +class MacroRecorder: + """An object for recording and running keyboard macros.""" + + def __init__(self): + self.macro = {} + self.recording_macro = None + self.macro_count = {} + + @cmdutils.register(instance='macro-recorder', name='record-macro') + @cmdutils.argument('win_id', win_id=True) + @cmdutils.argument('register') + 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 = objreg.get('mode-manager', scope='window', + window=win_id) + mode_manager.enter(usertypes.KeyMode.record_macro, + 'record_macro') + else: + self.record_macro(register) + else: + message.info("Macro recorded.") + self.recording_macro = None + + def record_macro(self, register): + """Start recording a macro.""" + message.info("Recording macro...") + self.macro[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) + @cmdutils.argument('register') + 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 = objreg.get('mode-manager', scope='window', + window=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.macro: + 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.macro[register]: + commandrunner.run_safely(*cmd) + + def record(self, text, count): + """Record a command if a macro is being recorded.""" + if self.recording_macro is not None: + self.macro[self.recording_macro].append((text, count)) + + +def init(): + """Initialize the MacroRecorder.""" + macro_recorder = MacroRecorder() + objreg.register('macro-recorder', macro_recorder) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index ba94e4696..17814be68 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -27,7 +27,6 @@ from PyQt5.QtCore import pyqtSlot, Qt from qutebrowser.config import config from qutebrowser.keyinput import keyparser -from qutebrowser.misc import utilcmds from qutebrowser.utils import usertypes, log, objreg, utils @@ -296,15 +295,16 @@ class RegisterKeyParser(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) elif self._mode == usertypes.KeyMode.record_macro: - utilcmds.record_macro(key) + macro_recorder.record_macro(key) elif self._mode == usertypes.KeyMode.run_macro: - utilcmds.run_macro(self._win_id, key) + macro_recorder.run_macro(self._win_id, key) else: raise ValueError( "{} is not a valid register key".format(self._mode)) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 4213dbc97..62202998c 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -251,61 +251,6 @@ def repeat_command(win_id, count=None): commandrunner.run(cmd[0], count if count is not None else cmd[1]) -@cmdutils.register(instance='mode-manager', name='record-macro', - scope='window') -@cmdutils.argument('register') -def record_macro_command(self, register=None): - """Start or stop recording a macro. - - Args: - register: Which register to store the macro in. - """ - if runners.recording_macro is None: - if register is None: - self.enter(usertypes.KeyMode.record_macro, 'record_macro') - else: - record_macro(register) - else: - message.info("Macro recorded.") - runners.recording_macro = None - - -def record_macro(register): - """Start recording a macro.""" - message.info("Recording macro...") - runners.macro[register] = [] - runners.recording_macro = register - - -@cmdutils.register(instance='mode-manager', name='run-macro', scope='window') -@cmdutils.argument('win_id', win_id=True) -@cmdutils.argument('count', count=True) -@cmdutils.argument('register') -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. - """ - runners.macro_count[win_id] = count - if register is None: - self.enter(usertypes.KeyMode.run_macro, 'run_macro') - else: - run_macro(win_id, register) - - -def run_macro(win_id, register): - """Run a recorded macro.""" - if register not in runners.macro: - raise cmdexc.CommandError( - "No macro recorded in '{}'!".format(register)) - commandrunner = runners.CommandRunner(win_id) - for _ in range(runners.macro_count[win_id]): - for cmd in runners.macro[register]: - commandrunner.run(*cmd) - - @cmdutils.register(debug=True, name='debug-log-capacity') def log_capacity(capacity: int): """Change the number of log lines to be stored in RAM. From 21289a80adda7c0226a493084f21ec6267c85edf Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Thu, 20 Oct 2016 17:37:02 +0200 Subject: [PATCH 5/8] Catch RegisterKeyParser command errors --- qutebrowser/keyinput/modeparsers.py | 30 +++++++++++++++++------------ tests/end2end/features/misc.feature | 15 +++++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 17814be68..7b8f4b207 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -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 = ":/?" @@ -297,17 +300,20 @@ class RegisterKeyParser(keyparser.BaseKeyParser): 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) - 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 key".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 key".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 register key") diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 0727f97cb..e66cfc94b 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -648,6 +648,21 @@ Feature: Various utility commands. 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 From 479c8e56b4a50e4db998073ded2affbe8b77a31c Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Wed, 9 Nov 2016 13:09:10 +0100 Subject: [PATCH 6/8] Improve macro code style, info messages --- qutebrowser/keyinput/macros.py | 48 +++++++++++++++++------------ qutebrowser/keyinput/modeparsers.py | 2 +- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py index 48c1f0a1d..c37885456 100644 --- a/qutebrowser/keyinput/macros.py +++ b/qutebrowser/keyinput/macros.py @@ -20,16 +20,26 @@ """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.""" + + """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.macro = {} - self.recording_macro = None - self.macro_count = {} + self._macros = {} + self._recording_macro = None + self._macro_count = {} @cmdutils.register(instance='macro-recorder', name='record-macro') @cmdutils.argument('win_id', win_id=True) @@ -40,23 +50,22 @@ class MacroRecorder: Args: register: Which register to store the macro in. """ - if self.recording_macro is None: + if self._recording_macro is None: if register is None: - mode_manager = objreg.get('mode-manager', scope='window', - window=win_id) + 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.") - self.recording_macro = None + 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...") - self.macro[register] = [] - self.recording_macro = register + 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) @@ -69,28 +78,27 @@ class MacroRecorder: count: How many times to run the macro. register: Which macro to run. """ - self.macro_count[win_id] = count + self._macro_count[win_id] = count if register is None: - mode_manager = objreg.get('mode-manager', scope='window', - window=win_id) + 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.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.macro[register]: + for _ in range(self._macro_count[win_id]): + for cmd in self._macros[register]: commandrunner.run_safely(*cmd) def record(self, text, count): """Record a command if a macro is being recorded.""" - if self.recording_macro is not None: - self.macro[self.recording_macro].append((text, count)) + if self._recording_macro is not None: + self._macros[self._recording_macro].append((text, count)) def init(): diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 7b8f4b207..b3ac47d3e 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -311,7 +311,7 @@ class RegisterKeyParser(keyparser.BaseKeyParser): macro_recorder.run_macro(self._win_id, key) else: raise ValueError( - "{} is not a valid register key".format(self._mode)) + "{} is not a valid register mode".format(self._mode)) except (cmdexc.CommandMetaError, cmdexc.CommandError) as err: message.error(str(err), stack=traceback.format_exc()) From f1c3bc89ec10daa11b8d89eae872a057841bdc75 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Wed, 9 Nov 2016 14:56:41 +0100 Subject: [PATCH 7/8] Further cleanup --- qutebrowser/commands/runners.py | 6 +++--- qutebrowser/keyinput/macros.py | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 6399d9d9f..cf19c0a4e 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -27,6 +27,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject from qutebrowser.config import config, configexc from qutebrowser.commands import cmdexc, cmdutils +from qutebrowser.keyinput import modeman from qutebrowser.utils import message, objreg, qtutils, usertypes, utils from qutebrowser.misc import split @@ -262,8 +263,7 @@ class CommandRunner(QObject): record_last_command = True record_macro = True - mode_manager = objreg.get('mode-manager', scope='window', - window=self._win_id) + mode_manager = modeman.instance(self._win_id) cur_mode = mode_manager.mode for result in self.parse_all(text): @@ -285,7 +285,7 @@ class CommandRunner(QObject): if record_macro and cur_mode == usertypes.KeyMode.normal: macro_recorder = objreg.get('macro-recorder') - macro_recorder.record(text, count) + macro_recorder.record_command(text, count) @pyqtSlot(str, int) @pyqtSlot(str) diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py index c37885456..8176e5652 100644 --- a/qutebrowser/keyinput/macros.py +++ b/qutebrowser/keyinput/macros.py @@ -43,7 +43,6 @@ class MacroRecorder: @cmdutils.register(instance='macro-recorder', name='record-macro') @cmdutils.argument('win_id', win_id=True) - @cmdutils.argument('register') def record_macro_command(self, win_id, register=None): """Start or stop recording a macro. @@ -70,7 +69,6 @@ class MacroRecorder: @cmdutils.register(instance='macro-recorder', name='run-macro') @cmdutils.argument('win_id', win_id=True) @cmdutils.argument('count', count=True) - @cmdutils.argument('register') def run_macro_command(self, win_id, count=1, register=None): """Run a recorded macro. @@ -95,7 +93,7 @@ class MacroRecorder: for cmd in self._macros[register]: commandrunner.run_safely(*cmd) - def record(self, text, count): + 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)) From a778b7184c7d524684bfd324c610908fc429b744 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Wed, 9 Nov 2016 19:07:56 +0100 Subject: [PATCH 8/8] Revert back to objreg to avoid circular import --- qutebrowser/commands/runners.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index cf19c0a4e..f6a8e07e2 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -27,7 +27,6 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject from qutebrowser.config import config, configexc from qutebrowser.commands import cmdexc, cmdutils -from qutebrowser.keyinput import modeman from qutebrowser.utils import message, objreg, qtutils, usertypes, utils from qutebrowser.misc import split @@ -263,7 +262,8 @@ class CommandRunner(QObject): record_last_command = True record_macro = True - mode_manager = modeman.instance(self._win_id) + mode_manager = objreg.get('mode-manager', scope='window', + window=self._win_id) cur_mode = mode_manager.mode for result in self.parse_all(text):