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