diff --git a/qutebrowser/app.py b/qutebrowser/app.py index acff48dc3..f00d71a78 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -180,13 +180,7 @@ class QuteBrowser(QApplication): Registers all commands, connects its signals, and sets up keyparser. """ - cmdutils.register_all() - for cmd in cmdutils.cmd_dict.values(): - cmd.signal.connect(self.cmd_handler) - try: - self.keyparser.from_config_sect(config.config['keybind']) - except KeyError: - pass + self.keyparser.from_config_sect(config.config['keybind']) def _process_init_args(self): """Process initial positional args. @@ -328,67 +322,7 @@ class QuteBrowser(QApplication): logging.debug("maybe_quit quitting.") self.quit() - @pyqtSlot(tuple) - def cmd_handler(self, tpl): - """Handle commands and delegate the specific actions. - - This gets called as a slot from all commands, and then calls the - appropriate command handler. - - All handlers supporting a count should have a keyword argument count. - - Args: - tpl: A tuple in the form (count, argv) where argv is - [cmd, arg, ...] - - Return: - The handlers return value. - - """ - (count, argv) = tpl - cmd = argv[0] - args = argv[1:] - browser = self.mainwindow.tabs - - handlers = { - 'open': browser.cur.openurl, - 'opencur': browser.opencur, - 'tabopen': browser.tabopen, - 'tabopencur': browser.tabopencur, - 'quit': self.shutdown, - 'tabclose': browser.tabclose, - 'tabprev': browser.switch_prev, - 'tabnext': browser.switch_next, - 'reload': browser.cur.reloadpage, - 'stop': browser.cur.stop, - 'back': browser.cur.back, - 'forward': browser.cur.forward, - 'print': browser.cur.printpage, - 'scroll': browser.cur.scroll, - 'scroll_page': browser.cur.scroll_page, - 'scroll_perc_x': browser.cur.scroll_percent_x, - 'scroll_perc_y': browser.cur.scroll_percent_y, - 'undo': browser.undo_close, - 'pyeval': self.pyeval, - 'settrace': set_trace, - 'nextsearch': self.searchparser.nextsearch, - 'yank': browser.cur.yank, - 'yanktitle': browser.cur.yank_title, - 'paste': browser.paste, - 'tabpaste': browser.tabpaste, - 'crash': self.crash, - 'version': lambda: browser.cur.openurl('qute:version'), - 'zoomin': browser.cur.zoom_in, - 'zoomout': browser.cur.zoom_out, - } - - handler = handlers[cmd] - - if count is not None and self.sender().count: - return handler(*args, count=count) - else: - return handler(*args) - + @cmdutils.register(split_args=False) def pyeval(self, s): """Evaluate a python string and display the results as a webpage. @@ -406,6 +340,7 @@ class QuteBrowser(QApplication): qutescheme.pyeval_output = out self.mainwindow.tabs.cur.openurl('qute:pyeval') + @cmdutils.register(hide=True) def crash(self): """Crash for debugging purposes. @@ -418,6 +353,7 @@ class QuteBrowser(QApplication): raise Exception("Forced crash") @pyqtSlot() + @cmdutils.register(name=['q', 'quit'], nargs=0) def shutdown(self, do_quit=True): """Try to shutdown everything cleanly. diff --git a/qutebrowser/commands/commands.py b/qutebrowser/commands/commands.py index 14fa89ac6..61a767575 100644 --- a/qutebrowser/commands/commands.py +++ b/qutebrowser/commands/commands.py @@ -30,290 +30,3 @@ A command class can set the following properties: hide -- If the command should be hidden in tab completion. Default: False """ - -from qutebrowser.commands.template import Command - - -class Open(Command): - - """Open a page in the current or [count]th tab. - - arg: The URL to open. - - """ - - nargs = 1 - split_args = False - count = True - - -class OpenCur(Command): - - """Fill the statusbar with :open and the current url.""" - - nargs = 0 - hide = True - - -class TabOpen(Command): - - """Open a page in a new tab. - - arg: The URL to open. - - """ - - nargs = 1 - split_args = False - - -class TabOpenCur(Command): - - """Fill the statusbar with :tabopen and the current url.""" - - nargs = 0 - hide = True - - -class TabClose(Command): - - """Close the current tab, or tab [count].""" - - nargs = 0 - count = True - - -class TabNext(Command): - - """Switch to the next tab, or skip [count] tabs.""" - - nargs = 0 - count = True - - -class TabPrev(Command): - - """Switch to the previous tab, or skip [count] tabs.""" - - nargs = 0 - count = True - - -class Quit(Command): - - """Quit qutebrowser.""" - - name = ['quit', 'q'] - nargs = 0 - - -class Reload(Command): - - """Reload the current page, or the page in tab [count].""" - - nargs = 0 - count = True - - -class Stop(Command): - - """Stop loading the current page, or the page in tab [count].""" - - nargs = 0 - count = True - - -class Back(Command): - - """Go back one/[count] page(s) in the history.""" - - nargs = 0 - count = True - - -class Forward(Command): - - """Go forward one/[count] page(s) in the history.""" - - nargs = 0 - count = True - - -class Print(Command): - - """Print the current page, or the page in tab [count].""" - - nargs = 0 - count = True - - -class Scroll(Command): - - """Scroll in x/y direction by a number of pixels. - - arg 1: delta x - arg 2: delta y - count: multiplicator - - """ - - nargs = 2 - count = True - hide = True - - -class ScrollPage(Command): - - """Scroll N pages up/down. - - arg 1: pages in x-direction - arg 2: pages in y-direction - count: multiplicator - - """ - - nargs = 2 - count = True - hide = True - name = 'scroll_page' - - -class Undo(Command): - - """Undo closing a tab.""" - - nargs = 0 - - -class ScrollPercX(Command): - - """Scroll N percent horizontally. - - optional arg: How many percent to scroll. - count: How many percent to scroll. - - """ - - nargs = '?' - count = True - hide = True - name = 'scroll_perc_x' - - -class ScrollPercY(Command): - - """Scroll N percent vertically. - - optional arg: How many percent to scroll. - count: How many percent to scroll. - - """ - - nargs = '?' - count = True - hide = True - name = 'scroll_perc_y' - - -class PyEval(Command): - - """Evaluate python code. - - arg: The python code to evaluate. - - """ - - nargs = 1 - split_args = False - - -class NextSearch(Command): - - """Jump to the next or [count]th next search term.""" - - nargs = 0 - hide = True - count = True - - -class Yank(Command): - - """Yank the URL of the current tab to the clipboard. - - optional arg: If 'sel', yank to the primary selection. - - """ - - nargs = '?' - - -class YankTitle(Command): - - """Yank the title of the current tab to the clipboard. - - optional arg: If 'sel', yank to the primary selection. - - """ - - nargs = '?' - - -class Paste(Command): - - """Open an URL from the clipboard. - - optional arg: If 'sel', paste from the primary selection. - - """ - - nargs = '?' - - -class TabPaste(Command): - - """Open an URL from the clipboard in a new tab. - - optional arg: If 'sel', paste from the primary selection. - - """ - - nargs = '?' - - -class ZoomIn(Command): - - """Zoom in in the current tab.""" - - nargs = 0 - count = True - - -class ZoomOut(Command): - - """Zoom out in the current tab.""" - - nargs = 0 - count = True - - -class Crash(Command): - - """Simply raise an exception for debugging.""" - - nargs = 0 - hide = True - - -class Version(Command): - - """Show version information.""" - - nargs = 0 - - -class SetTrace(Command): - - """Set pdb trace.""" - - nargs = 0 - hide = True diff --git a/qutebrowser/commands/template.py b/qutebrowser/commands/template.py index f67256bcc..ca9f4efaf 100644 --- a/qutebrowser/commands/template.py +++ b/qutebrowser/commands/template.py @@ -19,20 +19,15 @@ import logging -from PyQt5.QtCore import pyqtSignal, QObject - from qutebrowser.commands.exceptions import ArgumentCountError -class Command(QObject): +class Command: """Base skeleton for a command. - See the module documentation for qutebrowser.commands.commands for details. - - Signals: - signal: Emitted when the command was executed. - arg: A tuple (command, [args]) + Attributes: + FIXME ... """ @@ -40,13 +35,14 @@ class Command(QObject): # we should probably have some kind of typing / argument casting for args # this might be combined with help texts or so as well - def __init__(self, name, split_args, hide, nargs, count, handler): + def __init__(self, name, split_args, hide, nargs, count, desc, handler): super().__init__() self.name = name self.split_args = split_args self.hide = hide self.nargs = nargs self.count = count + self.desc = desc self.handler = handler def check(self, args): @@ -61,12 +57,11 @@ class Command(QObject): ArgumentCountError if the argument count is wrong. """ - if ((isinstance(self.nargs, int) and len(args) != self.nargs) or - (self.nargs == '?' and len(args) > 1) or - (self.nargs == '+' and len(args) < 1)): - # for nargs == '*', anything is okay - raise ArgumentCountError("{} args expected, but got {}".format( - self.nargs, len(args))) + if self.nargs[0] <= len(args) <= self.nargs[1]: + pass + else: + raise ArgumentCountError("{}-{} args expected, but got {}".format( + self.nargs[0], self.nargs[1], len(args))) def run(self, args=None, count=None): """Run the command. @@ -75,18 +70,15 @@ class Command(QObject): args: Arguments to the command. count: Command repetition count. - Emit: - The command's signal. - """ - dbgout = ["command called:", self.mainname] + dbgout = ["command called:", self.name] if args: dbgout += args if count is not None: dbgout.append("(count={})".format(count)) logging.debug(' '.join(dbgout)) - argv = [self.mainname] - if args is not None: - argv += args - self.signal.emit((count, argv)) + if count is not None and self.count: + return self.handler(*args, count=count) + else: + return self.handler(*args) diff --git a/qutebrowser/commands/utils.py b/qutebrowser/commands/utils.py index fd24d6264..6d01d39a8 100644 --- a/qutebrowser/commands/utils.py +++ b/qutebrowser/commands/utils.py @@ -23,8 +23,8 @@ import inspect from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject from PyQt5.QtWebKitWidgets import QWebPage -import qutebrowser.commands.commands import qutebrowser.config.config as config +from qutebrowser.commands.template import Command from qutebrowser.commands.exceptions import (ArgumentCountError, NoSuchCommandError) @@ -32,54 +32,81 @@ from qutebrowser.commands.exceptions import (ArgumentCountError, cmd_dict = {} -def register_cmd(func, name=None, split_args=True, hide=False): - """Decorator to register a new command handler.""" - global cmd_dict - names = [] - names += newnames - if name is None: - name = func.__name__.lower() - if isinstance(name, str): - mainname = name - names.append(name) - else: - mainname = name[0] - names += name - count, nargs = _get_nargs_count(func) - cmd = Command(mainname, split_args, hide, nargs, count, handler=func) - for name in names: - cmd_dict[name] = cmd - return func +class register: + """Decorator to register a new command handler. - -def _get_nargs_count(self, func): - """Get the number of command-arguments and count-support for a function. - - Args: - func: The function to get the argcount for. - - Return: - A (count, (minargs, maxargs)) tuple, with maxargs=None if there are - infinite args. count is True if the function supports count, else - False. - - Mapping from old nargs format to (minargs, maxargs): - ? (0, 1) - N (N, N) - + (1, None) - * (0, None) + This could also be a function, but as a class (with a "wrong" name) it's + much cleaner to implement. """ - # We could use inspect.signature maybe, but that's python >= 3.3 only. - spec = inspect.getfullargspec(func) - count = 'count' in spec.args - # we assume count always has a default (and it should!) - minargs = len(spec.args) - len(spec.defaults) - if spec.varargs is not None: - maxargs = None - else: - maxargs = len(spec.args) - int(count) # -1 if count is defined - return (count, (minargs, maxargs)) + + def __init__(self, name=None, nargs=None, split_args=True, hide=False): + self.name = name + self.split_args = split_args + self.hide = hide + self.nargs = nargs + + def __call__(self, func): + global cmd_dict + names = [] + name = func.__name__.lower() if self.name is None else self.name + if isinstance(name, str): + mainname = name + names.append(name) + else: + mainname = name[0] + names += name + count, nargs = self._get_nargs_count(func) + desc = func.__doc__.splitlines()[0].strip().rstrip('.') + cmd = Command(mainname, self.split_args, self.hide, nargs, count, desc, + handler=func) + for name in names: + cmd_dict[name] = cmd + return func + + + def _get_nargs_count(self, func): + """Get the number of command-arguments and count-support for a func. + + Args: + func: The function to get the argcount for. + + Return: + A (count, (minargs, maxargs)) tuple, with maxargs=None if there are + infinite args. count is True if the function supports count, else + False. + + Mapping from old nargs format to (minargs, maxargs): + ? (0, 1) + N (N, N) + + (1, None) + * (0, None) + + """ + # We could use inspect.signature maybe, but that's python >= 3.3 only. + spec = inspect.getfullargspec(func) + count = 'count' in spec.args + # we assume count always has a default (and it should!) + if self.nargs is not None: + # If nargs is overriden, use that. + if isinstance(self.nargs, int): + # Single int + minargs, maxargs = self.nargs, self.nargs + else: + # Tuple (min, max) + minargs, maxargs = self.nargs + else: + defaultcount = (len(spec.defaults) if spec.defaults is not None + else 0) + argcount = len(spec.args) + if 'self' in spec.args: + argcount -= 1 + minargs = argcount - defaultcount + if spec.varargs is not None: + maxargs = None + else: + maxargs = len(spec.args) - int(count) # -1 if count is defined + return (count, (minargs, maxargs)) class SearchParser(QObject): @@ -147,6 +174,7 @@ class SearchParser(QObject): """ self._search(text, rev=True) + @register(hide=True) def nextsearch(self, count=1): """Continue the search to the ([count]th) next term. diff --git a/qutebrowser/models/commandcompletion.py b/qutebrowser/models/commandcompletion.py index d8e4f7d72..28695732a 100644 --- a/qutebrowser/models/commandcompletion.py +++ b/qutebrowser/models/commandcompletion.py @@ -35,8 +35,7 @@ class CommandCompletionModel(CompletionModel): cmdlist = [] for obj in set(cmd_dict.values()): if not obj.hide: - doc = obj.__doc__.splitlines()[0].strip().rstrip('.') - cmdlist.append([obj.mainname, doc]) + cmdlist.append([obj.name, obj.desc]) data = OrderedDict() data['Commands'] = sorted(cmdlist) self.init_data(data) diff --git a/qutebrowser/utils/misc.py b/qutebrowser/utils/misc.py index 5e014bbee..2c85236a4 100644 --- a/qutebrowser/utils/misc.py +++ b/qutebrowser/utils/misc.py @@ -22,6 +22,8 @@ import os.path from PyQt5.QtCore import pyqtRemoveInputHook +#import qutebrowser.commands.utils as cmdutils + try: # pylint: disable=import-error from ipdb import set_trace as pdb_set_trace @@ -30,10 +32,10 @@ except ImportError: import qutebrowser - +# FIXME we can';t do this because of circular imports +#@cmdutils.register(name='settrace', hide=True) def set_trace(): - """ - Set a tracepoint in the Python debugger that works with Qt. + """Set a tracepoint in the Python debugger that works with Qt. Based on http://stackoverflow.com/a/1745965/2085149 diff --git a/qutebrowser/widgets/browser.py b/qutebrowser/widgets/browser.py index 39f0a1296..2128c4e97 100644 --- a/qutebrowser/widgets/browser.py +++ b/qutebrowser/widgets/browser.py @@ -36,6 +36,7 @@ from PyQt5.QtWebKitWidgets import QWebView, QWebPage import qutebrowser.utils.url as urlutils import qutebrowser.config.config as config +import qutebrowser.commands.utils as cmdutils from qutebrowser.widgets.tabbar import TabWidget from qutebrowser.network.networkmanager import NetworkManager from qutebrowser.utils.signals import SignalCache, dbg_signal @@ -246,6 +247,7 @@ class TabbedBrowser(TabWidget): tab.shutdown(callback=functools.partial(self._cb_tab_shutdown, tab)) + @cmdutils.register() def tabclose(self, count=None): """Close the current/[count]th tab. @@ -275,6 +277,7 @@ class TabbedBrowser(TabWidget): elif last_close == 'blank': tab.openurl('about:blank') + @cmdutils.register(split_args=False) def tabopen(self, url): """Open a new tab with a given url. @@ -308,6 +311,7 @@ class TabbedBrowser(TabWidget): tab.open_tab.connect(self.tabopen) tab.openurl(url) + @cmdutils.register(hide=True) def tabopencur(self): """Set the statusbar to :tabopen and the current URL. @@ -318,6 +322,7 @@ class TabbedBrowser(TabWidget): url = urlutils.urlstring(self.currentWidget().url()) self.set_cmd_text.emit(':tabopen ' + url) + @cmdutils.register(hide=True) def opencur(self): """Set the statusbar to :open and the current URL. @@ -328,8 +333,9 @@ class TabbedBrowser(TabWidget): url = urlutils.urlstring(self.currentWidget().url()) self.set_cmd_text.emit(':open ' + url) + @cmdutils.register(name='undo') def undo_close(self): - """Undo closing a tab. + """Switch to the previous tab, or skip [count] tabs. Command handler for :undo. @@ -337,6 +343,7 @@ class TabbedBrowser(TabWidget): if self._url_stack: self.tabopen(self._url_stack.pop()) + @cmdutils.register(name='tabprev') def switch_prev(self, count=1): """Switch to the ([count]th) previous tab. @@ -353,8 +360,9 @@ class TabbedBrowser(TabWidget): # FIXME pass + @cmdutils.register('tabnext') def switch_next(self, count=1): - """Switch to the ([count]th) next tab. + """Switch to the next tab, or skip [count] tabs. Command handler for :tabnext. @@ -369,6 +377,7 @@ class TabbedBrowser(TabWidget): # FIXME pass + @cmdutils.register() def paste(self, sel=False): """Open a page from the clipboard. @@ -385,6 +394,7 @@ class TabbedBrowser(TabWidget): logging.debug("Clipboard contained: '{}'".format(url)) self.openurl(url) + @cmdutils.register() def tabpaste(self, sel=False): """Open a page from the clipboard in a new tab. @@ -479,6 +489,7 @@ class CurCommandDispatcher(QObject): return frame.setScrollBarValue(orientation, int(m * perc / 100)) + @cmdutils.register(name='open', split_args=False) def openurl(self, url, count=None): """Open an url in the current/[count]th tab. @@ -501,6 +512,7 @@ class CurCommandDispatcher(QObject): else: tab.openurl(url) + @cmdutils.register(name='reload') def reloadpage(self, count=None): """Reload the current/[count]th tab. @@ -514,6 +526,7 @@ class CurCommandDispatcher(QObject): if tab is not None: tab.reload() + @cmdutils.register() def stop(self, count=None): """Stop loading in the current/[count]th tab. @@ -527,6 +540,7 @@ class CurCommandDispatcher(QObject): if tab is not None: tab.stop() + @cmdutils.register(name='print') def printpage(self, count=None): """Print the current/[count]th tab. @@ -543,6 +557,7 @@ class CurCommandDispatcher(QObject): preview.paintRequested.connect(tab.print) preview.exec_() + @cmdutils.register() def back(self, count=1): """Go back in the history of the current tab. @@ -556,6 +571,7 @@ class CurCommandDispatcher(QObject): for i in range(count): # pylint: disable=unused-variable self.tabs.currentWidget().back() + @cmdutils.register() def forward(self, count=1): """Go forward in the history of the current tab. @@ -580,6 +596,7 @@ class CurCommandDispatcher(QObject): """ self.tabs.currentWidget().findText(text, flags) + @cmdutils.register(hide=True) def scroll(self, dx, dy, count=1): """Scroll the current tab by count * dx/dy. @@ -595,8 +612,9 @@ class CurCommandDispatcher(QObject): dy = int(count) * float(dy) self.tabs.currentWidget().page_.mainFrame().scroll(dx, dy) + @cmdutils.register(name='scroll_perc_x', hide=True) def scroll_percent_x(self, perc=None, count=None): - """Scroll the current tab to a specific percent of the page. + """Scroll the current tab to a specific percent of the page (horiz). Command handler for :scroll_perc_x. @@ -607,8 +625,9 @@ class CurCommandDispatcher(QObject): """ self._scroll_percent(perc, count, Qt.Horizontal) + @cmdutils.register(name='scroll_perc_y', hide=True) def scroll_percent_y(self, perc=None, count=None): - """Scroll the current tab to a specific percent of the page. + """Scroll the current tab to a specific percent of the page (vert). Command handler for :scroll_perc_y @@ -619,6 +638,7 @@ class CurCommandDispatcher(QObject): """ self._scroll_percent(perc, count, Qt.Vertical) + @cmdutils.register(hide=True) def scroll_page(self, mx, my, count=1): """Scroll the frame page-wise. @@ -634,6 +654,7 @@ class CurCommandDispatcher(QObject): page.mainFrame().scroll(int(count) * float(mx) * size.width(), int(count) * float(my) * size.height()) + @cmdutils.register() def yank(self, sel=False): """Yank the current url to the clipboard or primary selection. @@ -653,6 +674,7 @@ class CurCommandDispatcher(QObject): self.temp_message.emit('URL yanked to {}'.format( 'primary selection' if sel else 'clipboard')) + @cmdutils.register(name='yanktitle') def yank_title(self, sel=False): """Yank the current title to the clipboard or primary selection. @@ -672,6 +694,7 @@ class CurCommandDispatcher(QObject): self.temp_message.emit('Title yanked to {}'.format( 'primary selection' if sel else 'clipboard')) + @cmdutils.register(name='zoomin') def zoom_in(self, count=1): """Zoom in in the current tab. @@ -682,6 +705,7 @@ class CurCommandDispatcher(QObject): tab = self.tabs.currentWidget() tab.zoom(count) + @cmdutils.register(name='zoomout') def zoom_out(self, count=1): """Zoom out in the current tab.