From a811f8cb07d87282e02a3f4777149a66bf72f22b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 2 Sep 2014 21:54:07 +0200 Subject: [PATCH 01/89] Start initial newcmd stuff. --- qutebrowser/app.py | 2 +- qutebrowser/browser/commands.py | 243 ++++++++++-------------------- qutebrowser/commands/argparser.py | 40 +++++ qutebrowser/commands/cmdutils.py | 229 +++++++++++++++++++--------- qutebrowser/commands/command.py | 67 ++++---- qutebrowser/commands/runners.py | 71 +++++---- qutebrowser/config/config.py | 27 +--- qutebrowser/config/configdata.py | 40 ++--- qutebrowser/utils/debug.py | 2 +- qutebrowser/utils/utilcmds.py | 5 +- qutebrowser/widgets/mainwindow.py | 2 +- qutebrowser/widgets/webview.py | 12 +- 12 files changed, 380 insertions(+), 360 deletions(-) create mode 100644 qutebrowser/commands/argparser.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index becd548f6..0b7dd2d13 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -581,7 +581,7 @@ class Application(QApplication): self._destroy_crashlogfile() sys.exit(1) - @cmdutils.register(instance='', nargs=0) + @cmdutils.register(instance='', ignore_args=True) def restart(self, shutdown=True, pages=None): """Restart qutebrowser while keeping existing tabs open.""" # We don't use _recover_pages here as it's too forgiving when diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 13173c674..9006c0fbc 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -78,9 +78,7 @@ class CommandDispatcher: if perc is None and count is None: perc = 100 elif perc is None: - perc = int(count) - else: - perc = float(perc) + perc = count perc = qtutils.check_overflow(perc, 'int', fatal=False) frame = self._current_widget().page().currentFrame() m = frame.scrollBarMaximum(orientation) @@ -161,28 +159,35 @@ class CommandDispatcher: @cmdutils.register(instance='mainwindow.tabs.cmd', name='open', split=False) - def openurl(self, urlstr, count=None): + def openurl(self, urlstr, bg=False, tab=False, count=None): """Open a URL in the current/[count]th tab. Args: urlstr: The URL to open, as string. + bg: Whether to open in a background tab. + tab: Whether to open in a tab. count: The tab index to open the URL in, or None. """ - tab = self._tabs.cntwidget(count) try: url = urlutils.fuzzy_url(urlstr) except urlutils.FuzzyUrlError as e: raise cmdexc.CommandError(e) - if tab is None: - if count is None: - # We want to open a URL in the current tab, but none exists - # yet. - self._tabs.tabopen(url) - else: - # Explicit count with a tab that doesn't exist. - return + if tab: + self._tabs.tabopen(url, background=False, explicit=True) + elif bg: + self._tabs.tabopen(url, background=True, explicit=True) else: - tab.openurl(url) + curtab = self._tabs.cntwidget(count) + if curtab is None: + if count is None: + # We want to open a URL in the current tab, but none exists + # yet. + self._tabs.tabopen(url) + else: + # Explicit count with a tab that doesn't exist. + return + else: + curtab.openurl(url) @cmdutils.register(instance='mainwindow.tabs.cmd', name='reload') def reloadpage(self, count=None): @@ -206,29 +211,12 @@ class CommandDispatcher: if tab is not None: tab.stop() - @cmdutils.register(instance='mainwindow.tabs.cmd') - def print_preview(self, count=None): - """Preview printing of the current/[count]th tab. - - Args: - count: The tab index to print, or None. - """ - if not qtutils.check_print_compat(): - # WORKAROUND (remove this when we bump the requirements to 5.3.0) - raise cmdexc.CommandError( - "Printing on Qt < 5.3.0 on Windows is broken, please upgrade!") - tab = self._tabs.cntwidget(count) - if tab is not None: - preview = QPrintPreviewDialog() - preview.setAttribute(Qt.WA_DeleteOnClose) - preview.paintRequested.connect(tab.print) - preview.exec_() - @cmdutils.register(instance='mainwindow.tabs.cmd', name='print') - def printpage(self, count=None): + def printpage(self, preview=False, count=None): """Print the current/[count]th tab. Args: + preview: Whether to preview instead of printing. count: The tab index to print, or None. """ if not qtutils.check_print_compat(): @@ -237,9 +225,15 @@ class CommandDispatcher: "Printing on Qt < 5.3.0 on Windows is broken, please upgrade!") tab = self._tabs.cntwidget(count) if tab is not None: - printdiag = QPrintDialog() - printdiag.setAttribute(Qt.WA_DeleteOnClose) - printdiag.open(lambda: tab.print(printdiag.printer())) + if preview: + diag = QPrintPreviewDialog() + diag.setAttribute(Qt.WA_DeleteOnClose) + diag.paintRequested.connect(tab.print) + diag.exec_() + else: + diag = QPrintDialog() + diag.setAttribute(Qt.WA_DeleteOnClose) + diag.open(lambda: tab.print(printdiag.printer())) @cmdutils.register(instance='mainwindow.tabs.cmd') def back(self, count=1): @@ -262,7 +256,8 @@ class CommandDispatcher: self._current_widget().go_forward() @cmdutils.register(instance='mainwindow.tabs.cmd') - def hint(self, group='all', target='normal', *args): + def hint(self, group=webelem.Group.all, target=hints.Target.normal, + *args : {'nargs': '*'}): """Start hinting. Args: @@ -281,9 +276,9 @@ class CommandDispatcher: - `yank-primary`: Yank the link to the primary selection. - `fill`: Fill the commandline with the command given as argument. - - `cmd-tab`: Fill the commandline with `:open-tab` and the + - `cmd-tab`: Fill the commandline with `:open -t` and the link. - - `cmd-tag-bg`: Fill the commandline with `:open-tab-bg` and + - `cmd-tag-bg`: Fill the commandline with `:open -b` and the link. - `rapid`: Open the link in a new tab and stay in hinting mode. - `download`: Download the link. @@ -305,18 +300,8 @@ class CommandDispatcher: frame = widget.page().mainFrame() if frame is None: raise cmdexc.CommandError("No frame focused!") - try: - group_enum = webelem.Group[group.replace('-', '_')] - except KeyError: - raise cmdexc.CommandError("Unknown hinting group {}!".format( - group)) - try: - target_enum = hints.Target[target.replace('-', '_')] - except KeyError: - raise cmdexc.CommandError("Unknown hinting target {}!".format( - target)) - widget.hintmanager.start(frame, self._tabs.current_url(), group_enum, - target_enum, *args) + widget.hintmanager.start(frame, self._tabs.current_url(), group, + target, *args) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) def follow_hint(self): @@ -324,43 +309,31 @@ class CommandDispatcher: self._current_widget().hintmanager.follow_hint() @cmdutils.register(instance='mainwindow.tabs.cmd') - def prev_page(self): + def prev_page(self, tab=False): """Open a "previous" link. This tries to automaticall click on typical "Previous Page" links using some heuristics. + + Args: + tab: Whether to open a new tab. """ - self._prevnext(prev=True, newtab=False) + self._prevnext(prev=True, newtab=tab) @cmdutils.register(instance='mainwindow.tabs.cmd') - def next_page(self): + def next_page(self, tab=False): """Open a "next" link. This tries to automatically click on typical "Next Page" links using some heuristics. + + Args: + tab: Whether to open a new tab. """ - self._prevnext(prev=False, newtab=False) - - @cmdutils.register(instance='mainwindow.tabs.cmd') - def prev_page_tab(self): - """Open a "previous" link in a new tab. - - This tries to automatically click on typical "Previous Page" links - using some heuristics. - """ - self._prevnext(prev=True, newtab=True) - - @cmdutils.register(instance='mainwindow.tabs.cmd') - def next_page_tab(self): - """Open a "next" link in a new tab. - - This tries to automatically click on typical "Previous Page" links - using some heuristics. - """ - self._prevnext(prev=False, newtab=True) + self._prevnext(prev=False, newtab=tab) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) - def scroll(self, dx, dy, count=1): + def scroll(self, dx : float, dy : float, count=1): """Scroll the current tab by 'count * dx/dy'. Args: @@ -368,40 +341,30 @@ class CommandDispatcher: dy: How much to scroll in x-direction. count: multiplier """ - dx = int(int(count) * float(dx)) - dy = int(int(count) * float(dy)) + dx *= count + dy *= count cmdutils.check_overflow(dx, 'int') cmdutils.check_overflow(dy, 'int') self._current_widget().page().currentFrame().scroll(dx, dy) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) - def scroll_perc_x(self, perc=None, count=None): - """Scroll horizontally to a specific percentage of the page. + def scroll_perc(self, perc : float = None, + horizontal : {'flag': 'x'} = False, count=None): + """Scroll to a specific percentage of the page. The percentage can be given either as argument or as count. If no percentage is given, the page is scrolled to the end. Args: perc: Percentage to scroll. + horizontal: Whether to scroll horizontally. count: Percentage to scroll. """ - self._scroll_percent(perc, count, Qt.Horizontal) + self._scroll_percent(perc, count, + Qt.Horizontal if horizontal else Qt.Vertical) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) - def scroll_perc_y(self, perc=None, count=None): - """Scroll vertically to a specific percentage of the page. - - The percentage can be given either as argument or as count. - If no percentage is given, the page is scrolled to the end. - - Args: - perc: Percentage to scroll. - count: Percentage to scroll. - """ - self._scroll_percent(perc, count, Qt.Vertical) - - @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) - def scroll_page(self, x, y, count=1): + def scroll_page(self, x : int, y : int, count=1): """Scroll the frame page-wise. Args: @@ -411,52 +374,36 @@ class CommandDispatcher: """ frame = self._current_widget().page().currentFrame() size = frame.geometry() - dx = int(count) * float(x) * size.width() - dy = int(count) * float(y) * size.height() + dx = count * x * size.width() + dy = count * y * size.height() cmdutils.check_overflow(dx, 'int') cmdutils.check_overflow(dy, 'int') frame.scroll(dx, dy) @cmdutils.register(instance='mainwindow.tabs.cmd') - def yank(self, sel=False): - """Yank the current URL to the clipboard or primary selection. + def yank(self, title=False, sel=False): + """Yank the current URL/title to the clipboard or primary selection. Args: sel: True to use primary selection, False to use clipboard + title: Whether to yank the title instead of the URL. """ clipboard = QApplication.clipboard() - urlstr = self._tabs.current_url().toString( - QUrl.FullyEncoded | QUrl.RemovePassword) + if title: + s = self._tabs.tabText(self._tabs.currentIndex()) + else: + s = self._tabs.current_url().toString( + QUrl.FullyEncoded | QUrl.RemovePassword) if sel and clipboard.supportsSelection(): mode = QClipboard.Selection target = "primary selection" else: mode = QClipboard.Clipboard target = "clipboard" - log.misc.debug("Yanking to {}: '{}'".format(target, urlstr)) - clipboard.setText(urlstr, mode) + log.misc.debug("Yanking to {}: '{}'".format(target, s)) + clipboard.setText(s, mode) message.info("URL yanked to {}".format(target)) - @cmdutils.register(instance='mainwindow.tabs.cmd') - def yank_title(self, sel=False): - """Yank the current title to the clipboard or primary selection. - - Args: - sel: True to use primary selection, False to use clipboard - """ - clipboard = QApplication.clipboard() - title = self._tabs.tabText(self._tabs.currentIndex()) - mode = QClipboard.Selection if sel else QClipboard.Clipboard - if sel and clipboard.supportsSelection(): - mode = QClipboard.Selection - target = "primary selection" - else: - mode = QClipboard.Clipboard - target = "clipboard" - log.misc.debug("Yanking to {}: '{}'".format(target, title)) - clipboard.setText(title, mode) - message.info("Title yanked to {}".format(target)) - @cmdutils.register(instance='mainwindow.tabs.cmd') def zoom_in(self, count=1): """Increase the zoom level for the current tab. @@ -503,24 +450,6 @@ class CommandDispatcher: continue self._tabs.close_tab(tab) - @cmdutils.register(instance='mainwindow.tabs.cmd', split=False) - def open_tab(self, urlstr): - """Open a new tab with a given url.""" - try: - url = urlutils.fuzzy_url(urlstr) - except urlutils.FuzzyUrlError as e: - raise cmdexc.CommandError(e) - self._tabs.tabopen(url, background=False, explicit=True) - - @cmdutils.register(instance='mainwindow.tabs.cmd', split=False) - def open_tab_bg(self, urlstr): - """Open a new tab in background.""" - try: - url = urlutils.fuzzy_url(urlstr) - except urlutils.FuzzyUrlError as e: - raise cmdexc.CommandError(e) - self._tabs.tabopen(url, background=True, explicit=True) - @cmdutils.register(instance='mainwindow.tabs.cmd') def undo(self): """Re-open a closed tab (optionally skipping [count] closed tabs).""" @@ -559,7 +488,7 @@ class CommandDispatcher: else: raise cmdexc.CommandError("Last tab") - @cmdutils.register(instance='mainwindow.tabs.cmd', nargs=(0, 1)) + @cmdutils.register(instance='mainwindow.tabs.cmd') def paste(self, sel=False, tab=False): """Open a page from the clipboard. @@ -589,16 +518,7 @@ class CommandDispatcher: widget.openurl(url) @cmdutils.register(instance='mainwindow.tabs.cmd') - def paste_tab(self, sel=False): - """Open a page from the clipboard in a new tab. - - Args: - sel: True to use primary selection, False to use clipboard - """ - self.paste(sel, True) - - @cmdutils.register(instance='mainwindow.tabs.cmd') - def tab_focus(self, index=None, count=None): + def tab_focus(self, index : int = None, count=None): """Select the tab given as argument/[count]. Args: @@ -622,7 +542,7 @@ class CommandDispatcher: idx)) @cmdutils.register(instance='mainwindow.tabs.cmd') - def tab_move(self, direction=None, count=None): + def tab_move(self, direction : ('+', '-') = None, count=None): """Move the current tab. Args: @@ -698,26 +618,19 @@ class CommandDispatcher: quickmarks.prompt_save(self._tabs.current_url()) @cmdutils.register(instance='mainwindow.tabs.cmd') - def quickmark_load(self, name): + def quickmark_load(self, name, tab=False, bg=False): """Load a quickmark.""" urlstr = quickmarks.get(name) url = QUrl(urlstr) if not url.isValid(): raise cmdexc.CommandError("Invalid URL {} ({})".format( urlstr, url.errorString())) - self._current_widget().openurl(url) - - @cmdutils.register(instance='mainwindow.tabs.cmd') - def quickmark_load_tab(self, name): - """Load a quickmark in a new tab.""" - url = quickmarks.get(name) - self._tabs.tabopen(url, background=False, explicit=True) - - @cmdutils.register(instance='mainwindow.tabs.cmd') - def quickmark_load_tab_bg(self, name): - """Load a quickmark in a new background tab.""" - url = quickmarks.get(name) - self._tabs.tabopen(url, background=True, explicit=True) + if tab: + self._tabs.tabopen(url, background=False, explicit=True) + elif bg: + self._tabs.tabopen(url, background=True, explicit=True) + else: + self._current_widget().openurl(url) @cmdutils.register(instance='mainwindow.tabs.cmd', name='inspector') def toggle_inspector(self): diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py new file mode 100644 index 000000000..a31f0988d --- /dev/null +++ b/qutebrowser/commands/argparser.py @@ -0,0 +1,40 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014 Florian Bruhin (The Compiler) +# +# 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 . + +"""argparse.ArgumentParser subclass to parse qutebrowser commands.""" + +import argparse + + +class ArgumentParserError(Exception): + + """Exception raised when the ArgumentParser signals an error.""" + + +class ArgumentParser(argparse.ArgumentParser): + + def __init__(self): + super().__init__(add_help=False) + + def exit(self, status=0, msg=None): + raise AssertionError('exit called, this should never happen. ' + 'Status: {}, message: {}'.format(status, msg)) + + def error(self, msg): + raise ArgumentParserError(msg) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 4fe0cc9d1..4735bad53 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -23,11 +23,12 @@ Module attributes: cmd_dict: A mapping from command-strings to command objects. """ +import enum import inspect import collections -from qutebrowser.utils import usertypes, qtutils -from qutebrowser.commands import command, cmdexc +from qutebrowser.utils import usertypes, qtutils, log +from qutebrowser.commands import command, cmdexc, argparser cmd_dict = {} @@ -68,23 +69,19 @@ def arg_or_count(arg, count, default=None, countzero=None): The value to use. Raise: - ValueError: If nothing was set or the value couldn't be converted to - an integer. + ValueError: If nothing was set. """ if count is not None and arg is not None: raise ValueError("Both count and argument given!") elif arg is not None: - try: - return int(arg) - except ValueError: - raise ValueError("Invalid number: {}".format(arg)) + return arg elif count is not None: if countzero is not None and count == 0: return countzero else: - return int(count) + return count elif default is not None: - return int(default) + return default else: raise ValueError("Either count or argument have to be set!") @@ -99,7 +96,6 @@ class register: # pylint: disable=invalid-name Attributes: instance: The instance to be used as "self", as a dotted string. name: The name (as string) or names (as list) of the command. - nargs: A (minargs, maxargs) tuple of valid argument counts, or an int. split: Whether to split the arguments. hide: Whether to hide the command or not. completion: Which completion to use for arguments, as a list of @@ -107,11 +103,18 @@ class register: # pylint: disable=invalid-name modes/not_modes: List of modes to use/not use. needs_js: If javascript is needed for this command. debug: Whether this is a debugging command (only shown with --debug). + ignore_args: Whether to ignore the arguments of the function. + + Class attributes: + AnnotationInfo: Named tuple for info from an annotation. """ - def __init__(self, instance=None, name=None, nargs=None, split=True, - hide=False, completion=None, modes=None, not_modes=None, - needs_js=False, debug=False): + AnnotationInfo = collections.namedtuple('AnnotationInfo', + 'kwargs, typ, name, flag') + + def __init__(self, instance=None, name=None, split=True, hide=False, + completion=None, modes=None, not_modes=None, needs_js=False, + debug=False, ignore_args=False): """Save decorator arguments. Gets called on parse-time with the decorator arguments. @@ -125,13 +128,13 @@ class register: # pylint: disable=invalid-name self.name = name self.split = split self.hide = hide - self.nargs = nargs self.instance = instance self.completion = completion self.modes = modes self.not_modes = not_modes self.needs_js = needs_js self.debug = debug + self.ignore_args = ignore_args if modes is not None: for m in modes: if not isinstance(m, usertypes.KeyMode): @@ -155,74 +158,156 @@ class register: # pylint: disable=invalid-name Return: The original function (unmodified). """ - names = [] - if self.name is None: - name = func.__name__.lower().replace('_', '-') - else: - name = self.name - if isinstance(name, str): - mainname = name - names.append(name) - else: - mainname = name[0] - names += name - if mainname in cmd_dict: + names = self._get_names(func) + log.commands.vdebug("Registering command {}".format(names[0])) + if any(name in cmd_dict for name in names): raise ValueError("{} is already registered!".format(name)) - argspec = inspect.getfullargspec(func) - if 'self' in argspec.args and self.instance is None: - raise ValueError("{} is a class method, but instance was not " - "given!".format(mainname)) - count, nargs = self._get_nargs_count(argspec) - if func.__doc__ is not None: - desc = func.__doc__.splitlines()[0].strip() - else: - desc = "" + has_count, desc, parser = self._inspect_func(func) cmd = command.Command( - name=mainname, split=self.split, hide=self.hide, nargs=nargs, - count=count, desc=desc, instance=self.instance, handler=func, + name=names[0], split=self.split, hide=self.hide, count=has_count, + desc=desc, instance=self.instance, handler=func, completion=self.completion, modes=self.modes, - not_modes=self.not_modes, needs_js=self.needs_js, debug=self.debug) + not_modes=self.not_modes, needs_js=self.needs_js, debug=self.debug, + parser=parser) for name in names: cmd_dict[name] = cmd return func - def _get_nargs_count(self, spec): - """Get the number of command-arguments and count-support for a func. + def _get_names(self, func): + """Get the name(s) which should be used for the current command. + + If the name hasn't been overridden explicitely, the function name is + transformed. + + If it has been set, it can either be a string which is + used directly, or an iterable. Args: - spec: A FullArgSpec as returned by inspect. + func: The function to get the name 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) + A list of names, with the main name being the first item. """ - 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, collections.Iterable): - # Iterable (min, max) - # pylint: disable=unpacking-non-sequence - minargs, maxargs = self.nargs - else: - # Single int - minargs, maxargs = self.nargs, self.nargs + if self.name is None: + return [func.__name__.lower().replace('_', '-')] + elif isinstance(self.name, str): + return [self.name] 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 + return self.name + + def _inspect_func(self, func): + """Inspect a function to get useful informations from it. + + Args: + func: The function to look at. + + Return: + A (has_count, desc, parser) tuple. + has_count: Whether the command supports a count. + desc: The description of the command. + parser: The ArgumentParser to use when parsing the commandline. + """ + signature = inspect.signature(func) + if 'self' in signature.parameters and self.instance is None: + raise ValueError("{} is a class method, but instance was not " + "given!".format(mainname)) + has_count = 'count' in signature.parameters + parser = argparser.ArgumentParser() + if func.__doc__ is not None: + desc = func.__doc__.splitlines()[0].strip() + else: + desc = "" + if not self.ignore_args: + for param in signature.parameters.values(): + if param.name in ('self', 'count'): + continue + args = [] + kwargs = {} + annotation_info = self._parse_annotation(param) + if annotation_info.typ is not None: + typ = annotation_info.typ + else: + typ = self._infer_type(param) + kwargs.update(self._type_to_argparse(typ)) + kwargs.update(annotation_info.kwargs) + if (param.kind == inspect.Parameter.VAR_POSITIONAL and + 'nargs' not in kwargs): # annotation_info overrides it + kwargs['nargs'] = '*' + is_flag = typ == bool + args += self._get_argparse_args(param, annotation_info, + is_flag) + log.commands.vdebug('Adding argument {} of type {} -> ' + 'args {}, kwargs {}'.format( + param.name, typ, args, kwargs)) + parser.add_argument(*args, **kwargs) + return has_count, desc, parser + + def _get_argparse_args(self, param, annotation_info, is_flag): + """Get a list of positional argparse arguments. + + Args: + param: The inspect.Parameter instance for the current parameter. + annotation_info: An AnnotationInfo tuple for the parameter. + is_flag: Whether the option is a flag or not. + """ + args = [] + name = annotation_info.name or param.name + shortname = annotation_info.flag or param.name[0] + if is_flag: + args.append('--{}'.format(name)) + args.append('-{}'.format(shortname)) + else: + args.append(name) + return args + + def _parse_annotation(self, param): + """Get argparse arguments and type from a parameter annotation. + + Args: + param: A inspect.Parameter instance. + + Return: + An AnnotationInfo namedtuple. + kwargs: A dict of keyword args to add to the + argparse.ArgumentParser.add_argument call. + typ: The type to use for this argument. + flag: The short name/flag if overridden. + name: The long name if overridden. + """ + info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None} + log.commands.vdebug("Parsing annotation {}".format(param.annotation)) + if param.annotation is not inspect.Parameter.empty: + if isinstance(param.annotation, dict): + for field in ('type', 'flag', 'name'): + if field in param.annotation: + info[field] = param.annotation[field] + del param.annotation[field] + info['kwargs'] = param.annotation else: - maxargs = argcount - int(count) # -1 if count is defined - return (count, (minargs, maxargs)) + info['typ'] = param.annotation + return self.AnnotationInfo(**info) + + def _infer_type(self, param): + """Get the type of an argument from its default value.""" + if param.default is None or param.default is inspect.Parameter.empty: + return None + else: + return type(param.default) + + def _type_to_argparse(self, typ): + """Get argparse keyword arguments based on a type.""" + kwargs = {} + try: + is_enum = issubclass(typ, enum.Enum) + except TypeError: + is_enum = False + if isinstance(typ, tuple): + kwargs['choices'] = typ + elif is_enum: + kwargs['choices'] = [e.name.replace('_', '-') for e in typ] + elif typ is bool: + kwargs['action'] = 'store_true' + elif typ is not None: + kwargs['type'] = typ + + return kwargs diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 4d774198f..0780c3519 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -19,11 +19,11 @@ """Contains the Command class, a skeleton for a command.""" -from PyQt5.QtCore import QCoreApplication, QUrl +from PyQt5.QtCore import QCoreApplication from PyQt5.QtWebKit import QWebSettings -from qutebrowser.commands import cmdexc -from qutebrowser.utils import log, utils +from qutebrowser.commands import cmdexc, argparser +from qutebrowser.utils import log, utils, message class Command: @@ -34,7 +34,6 @@ class Command: name: The main name of the command. split: Whether to split the arguments. hide: Whether to hide the arguments or not. - nargs: A (minargs, maxargs) tuple, maxargs = None if there's no limit. count: Whether the command supports a count, or not. desc: The description of the command. instance: How to get to the "self" argument of the handler. @@ -43,20 +42,20 @@ class Command: completion: Completions to use for arguments, as a list of strings. needs_js: Whether the command needs javascript enabled debug: Whether this is a debugging command (only shown with --debug). + parser: The ArgumentParser to use to parse this command. """ # TODO: # 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, hide, nargs, count, desc, instance, - handler, completion, modes, not_modes, needs_js, debug): + def __init__(self, name, split, hide, count, desc, instance, handler, + completion, modes, not_modes, needs_js, debug, parser): # I really don't know how to solve this in a better way, I tried. # pylint: disable=too-many-arguments self.name = name self.split = split self.hide = hide - self.nargs = nargs self.count = count self.desc = desc self.instance = instance @@ -66,15 +65,12 @@ class Command: self.not_modes = not_modes self.needs_js = needs_js self.debug = debug + self.parser = parser - def check(self, args): - """Check if the argument count is valid and the command is permitted. - - Args: - args: The supplied arguments + def _check_prerequisites(self): + """Check if the command is permitted to run currently. Raise: - ArgumentCountError if the argument count is wrong. PrerequisitesError if the command can't be called currently. """ # We don't use modeman.instance() here to avoid a circular import @@ -94,20 +90,6 @@ class Command: QWebSettings.JavascriptEnabled): raise cmdexc.PrerequisitesError( "{}: This command needs javascript enabled.".format(self.name)) - if self.nargs[1] is None and self.nargs[0] <= len(args): - pass - elif self.nargs[0] <= len(args) <= self.nargs[1]: - pass - else: - if self.nargs[0] == self.nargs[1]: - argcnt = str(self.nargs[0]) - elif self.nargs[1] is None: - argcnt = '{}-inf'.format(self.nargs[0]) - else: - argcnt = '{}-{}'.format(self.nargs[0], self.nargs[1]) - raise cmdexc.ArgumentCountError( - "{}: {} args expected, but got {}".format(self.name, argcnt, - len(args))) def run(self, args=None, count=None): """Run the command. @@ -120,23 +102,29 @@ class Command: """ dbgout = ["command called:", self.name] if args: - dbgout += args + dbgout.append(str(args)) if count is not None: dbgout.append("(count={})".format(count)) log.commands.debug(' '.join(dbgout)) + posargs = [] kwargs = {} app = QCoreApplication.instance() - # Replace variables (currently only {url}) - new_args = [] - for arg in args: - if arg == '{url}': - urlstr = app.mainwindow.tabs.current_url().toString( - QUrl.FullyEncoded | QUrl.RemovePassword) - new_args.append(urlstr) + try: + namespace = self.parser.parse_args(args) + except argparser.ArgumentParserError as e: + message.error(str(e)) + return + + for name, arg in vars(namespace).items(): + if isinstance(arg, list): + # If we got a list, we assume that's our *args, so we don't add + # it to kwargs. + # FIXME: This approach is rather naive, but for now it works. + posargs += arg else: - new_args.append(arg) + kwargs[name] = arg if self.instance is not None: # Add the 'self' parameter. @@ -144,9 +132,12 @@ class Command: obj = app else: obj = utils.dotted_getattr(app, self.instance) - new_args.insert(0, obj) + posargs.insert(0, obj) if count is not None and self.count: kwargs = {'count': count} - self.handler(*new_args, **kwargs) + self._check_prerequisites() + log.commands.debug('posargs: {}'.format(posargs)) + log.commands.debug('kwargs: {}'.format(kwargs)) + self.handler(*posargs, **kwargs) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 95d00e4b7..7ad83ed9d 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -19,7 +19,7 @@ """Module containing command managers (SearchRunner and CommandRunner).""" -from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject +from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QCoreApplication, QUrl from PyQt5.QtWebKitWidgets import QWebPage from qutebrowser.config import config @@ -198,14 +198,18 @@ class CommandRunner: parts = text.strip().split(maxsplit=1) if not parts: raise cmdexc.NoSuchCommandError("No command given") - cmdstr = parts[0] + elif len(parts) > 1: + cmdstr, argstr = parts + else: + cmdstr = parts[0] + argstr = None if aliases: new_cmd = self._get_alias(text, alias_no_args) if new_cmd is not None: log.commands.debug("Re-parsing with '{}'.".format(new_cmd)) return self.parse(new_cmd, aliases=False) try: - cmd = cmdutils.cmd_dict[cmdstr] + self._cmd = cmdutils.cmd_dict[cmdstr] except KeyError: if fallback: parts = text.split(' ') @@ -215,36 +219,38 @@ class CommandRunner: else: raise cmdexc.NoSuchCommandError( '{}: no such command'.format(cmdstr)) - if len(parts) == 1: - args = [] - elif cmd.split: - args = utils.safe_shlex_split(parts[1]) + if argstr is None: + self._args = [] + elif self._cmd.split: + self._args = utils.safe_shlex_split(argstr) else: - args = parts[1].split(maxsplit=cmd.nargs[0] - 1) - self._cmd = cmd - self._args = args - retargs = args[:] + # If split=False, we still want to split the flags, but not + # everything after that. + # We first split the arg string and check the index of the first + # non-flag args, then we re-split again properly. + # example: + # + # input: "--foo -v bar baz" + # first split: ['--foo', '-v', 'bar', 'baz'] + # 0 1 2 3 + # second split: ['--foo', '-v', 'bar baz'] + # (maxsplit=2) + split_args = argstr.split() + for i, arg in enumerate(split_args): + if not arg.startswith('-'): + self._args = argstr.split(maxsplit=i) + break + else: + # If there are only flags, we got it right on the first try + # already. + self._args = split_args + retargs = self._args[:] if text.endswith(' '): retargs.append('') return [cmdstr] + retargs - def _check(self): - """Check if the argument count for the command is correct.""" - self._cmd.check(self._args) - - def _run(self, count=None): - """Run a command with an optional count. - - Args: - count: Count to pass to the command. - """ - if count is not None: - self._cmd.run(self._args, count=count) - else: - self._cmd.run(self._args) - def run(self, text, count=None): - """Parse a command from a line of text. + """Parse a command from a line of text and run it. Args: text: The text to parse. @@ -255,8 +261,15 @@ class CommandRunner: self.run(sub, count) return self.parse(text) - self._check() - self._run(count=count) + app = QCoreApplication.instance() + cur_url = app.mainwindow.tabs.current_url().toString( + QUrl.FullyEncoded | QUrl.RemovePassword) + self._args = [cur_url if e == '{url}' else e for e in self._args] + if count is not None: + self._cmd.run(self._args, count=count) + else: + self._cmd.run(self._args) + @pyqtSlot(str, int) def run_safely(self, text, count=None): diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 986afd62f..3d0f6516e 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -336,7 +336,7 @@ class ConfigManager(QObject): @cmdutils.register(name='set', instance='config', completion=[Completion.section, Completion.option, Completion.value]) - def set_wrapper(self, sectname, optname, value): + def set_command(self, sectname, optname, value, temp=False): """Set an option. // @@ -347,36 +347,15 @@ class ConfigManager(QObject): sectname: The section where the option is in. optname: The name of the option. value: The value to set. + temp: Set value temporarely. """ try: - self.set('conf', sectname, optname, value) + self.set('temp' if temp else 'conf', sectname, optname, value) except (NoOptionError, NoSectionError, configtypes.ValidationError, ValueError) as e: raise cmdexc.CommandError("set: {} - {}".format( e.__class__.__name__, e)) - @cmdutils.register(name='set-temp', instance='config', - completion=[Completion.section, Completion.option, - Completion.value]) - def set_temp_wrapper(self, sectname, optname, value): - """Set a temporary option. - - // - - Wrapper for self.set() to output exceptions in the status bar. - - Args: - sectname: The section where the option is in. - optname: The name of the option. - value: The value to set. - """ - try: - self.set('temp', sectname, optname, value) - except (NoOptionError, NoSectionError, - configtypes.ValidationError) as e: - raise cmdexc.CommandError("set: {} - {}".format( - e.__class__.__name__, e)) - def set(self, layer, sectname, optname, value): """Set an option. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 2c79bd209..7afff556c 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -585,11 +585,11 @@ DATA = collections.OrderedDict([ typ.KeyBindingName(), typ.KeyBinding(), ('o', 'set-cmd-text ":open "'), ('go', 'set-cmd-text :open {url}'), - ('O', 'set-cmd-text ":open-tab "'), - ('gO', 'set-cmd-text :open-tab {url}'), - ('xo', 'set-cmd-text ":open-tab-bg "'), - ('xO', 'set-cmd-text :open-tab-bg {url}'), - ('ga', 'open-tab about:blank'), + ('O', 'set-cmd-text ":open -t "'), + ('gO', 'set-cmd-text :open -t {url}'), + ('xo', 'set-cmd-text ":open -b "'), + ('xO', 'set-cmd-text :open -b {url}'), + ('ga', 'open -t about:blank'), ('d', 'tab-close'), ('co', 'tab-only'), ('T', 'tab-focus'), @@ -608,8 +608,8 @@ DATA = collections.OrderedDict([ (';I', 'hint images tab'), ('.i', 'hint images tab-bg'), (';o', 'hint links fill :open {hint-url}'), - (';O', 'hint links fill :open-tab {hint-url}'), - ('.o', 'hint links fill :open-tab-bg {hint-url}'), + (';O', 'hint links fill :open -t {hint-url}'), + ('.o', 'hint links fill :open -b {hint-url}'), (';y', 'hint links yank'), (';Y', 'hint links yank-primary'), (';r', 'hint links rapid'), @@ -619,33 +619,33 @@ DATA = collections.OrderedDict([ ('k', 'scroll 0 -50'), ('l', 'scroll 50 0'), ('u', 'undo'), - ('gg', 'scroll-perc-y 0'), - ('G', 'scroll-perc-y'), + ('gg', 'scroll-perc 0'), + ('G', 'scroll-perc'), ('n', 'search-next'), ('N', 'search-prev'), ('i', 'enter-mode insert'), ('yy', 'yank'), - ('yY', 'yank sel'), - ('yt', 'yank-title'), - ('yT', 'yank-title sel'), + ('yY', 'yank -s'), + ('yt', 'yank -t'), + ('yT', 'yank -ts'), ('pp', 'paste'), - ('pP', 'paste sel'), - ('Pp', 'paste-tab'), - ('PP', 'paste-tab sel'), + ('pP', 'paste -s'), + ('Pp', 'paste -t'), + ('PP', 'paste -ts'), ('m', 'quickmark-save'), ('b', 'set-cmd-text ":quickmark-load "'), - ('B', 'set-cmd-text ":quickmark-load-tab "'), + ('B', 'set-cmd-text ":quickmark-load -t "'), ('sf', 'save'), ('ss', 'set-cmd-text ":set "'), - ('sl', 'set-cmd-text ":set-temp "'), + ('sl', 'set-cmd-text ":set -t"'), ('sk', 'set-cmd-text ":set keybind "'), ('-', 'zoom-out'), ('+', 'zoom-in'), ('=', 'zoom'), ('[[', 'prev-page'), (']]', 'next-page'), - ('{{', 'prev-page-tab'), - ('}}', 'next-page-tab'), + ('{{', 'prev-page -t'), + ('}}', 'next-page -t'), ('wi', 'inspector'), ('gd', 'download-page'), ('ad', 'cancel-download'), @@ -654,7 +654,7 @@ DATA = collections.OrderedDict([ ('', 'quit'), ('', 'undo'), ('', 'tab-close'), - ('', 'open-tab about:blank'), + ('', 'open -t about:blank'), ('', 'scroll-page 0 1'), ('', 'scroll-page 0 -1'), ('', 'scroll-page 0 0.5'), diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index d00473437..aa7641143 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -32,7 +32,7 @@ from qutebrowser.config import config, style @cmdutils.register(debug=True) -def debug_crash(typ='exception'): +def debug_crash(typ : ('exception', 'segfault') = 'exception'): """Crash for debugging purposes. Args: diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py index 0bb31769c..82ef05117 100644 --- a/qutebrowser/utils/utilcmds.py +++ b/qutebrowser/utils/utilcmds.py @@ -35,15 +35,14 @@ def init(): _commandrunner = runners.CommandRunner() -@cmdutils.register(nargs=(2, None)) -def later(ms, *command): +@cmdutils.register() +def later(ms : int, *command : {'nargs': '+'}): """Execute a command after some time. Args: ms: How many milliseconds to wait. command: The command/args to run. """ - ms = int(ms) timer = usertypes.Timer(name='later') timer.setSingleShot(True) if ms < 0: diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py index 32ad27374..125acd2bd 100644 --- a/qutebrowser/widgets/mainwindow.py +++ b/qutebrowser/widgets/mainwindow.py @@ -141,7 +141,7 @@ class MainWindow(QWidget): if rect.isValid(): self.completion.setGeometry(rect) - @cmdutils.register(instance='mainwindow', name=['quit', 'q'], nargs=0) + @cmdutils.register(instance='mainwindow', name=['quit', 'q']) def close(self): """Quit qutebrowser. diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index 34b8f3016..b94d74759 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -221,20 +221,20 @@ class WebView(QWebView): e: The QMouseEvent. """ if self._force_open_target is not None: - self.open_target = self._force_open_target + self._open_target = self._force_open_target self._force_open_target = None log.mouse.debug("Setting force target: {}".format( - self.open_target)) + self._open_target)) elif (e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier): if config.get('tabs', 'background-tabs'): - self.open_target = usertypes.ClickTarget.tab_bg + self._open_target = usertypes.ClickTarget.tab_bg else: - self.open_target = usertypes.ClickTarget.tab + self._open_target = usertypes.ClickTarget.tab log.mouse.debug("Middle click, setting target: {}".format( - self.open_target)) + self._open_target)) else: - self.open_target = usertypes.ClickTarget.normal + self._open_target = usertypes.ClickTarget.normal log.mouse.debug("Normal click, setting normal target") def shutdown(self): From d836e26107733e558a8e8553f6cc977d8aeac18c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 Sep 2014 08:57:21 +0200 Subject: [PATCH 02/89] Nicer debug printing of functions. --- qutebrowser/commands/cmdutils.py | 9 +++-- qutebrowser/commands/command.py | 6 +-- qutebrowser/utils/debug.py | 38 ++++++++++++++++-- qutebrowser/utils/utilcmds.py | 68 +++++++++++++++++++++++++++++++- 4 files changed, 110 insertions(+), 11 deletions(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 4735bad53..7302fbfec 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -27,7 +27,7 @@ import enum import inspect import collections -from qutebrowser.utils import usertypes, qtutils, log +from qutebrowser.utils import usertypes, qtutils, log, debug from qutebrowser.commands import command, cmdexc, argparser cmd_dict = {} @@ -236,9 +236,10 @@ class register: # pylint: disable=invalid-name is_flag = typ == bool args += self._get_argparse_args(param, annotation_info, is_flag) - log.commands.vdebug('Adding argument {} of type {} -> ' - 'args {}, kwargs {}'.format( - param.name, typ, args, kwargs)) + callsig = debug.format_call(parser.add_argument, args, + kwargs, full=False) + log.commands.vdebug('Adding arg {} of type {} -> {}'.format( + param.name, typ, callsig)) parser.add_argument(*args, **kwargs) return has_count, desc, parser diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 0780c3519..afb3b30e7 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -23,7 +23,7 @@ from PyQt5.QtCore import QCoreApplication from PyQt5.QtWebKit import QWebSettings from qutebrowser.commands import cmdexc, argparser -from qutebrowser.utils import log, utils, message +from qutebrowser.utils import log, utils, message, debug class Command: @@ -138,6 +138,6 @@ class Command: kwargs = {'count': count} self._check_prerequisites() - log.commands.debug('posargs: {}'.format(posargs)) - log.commands.debug('kwargs: {}'.format(kwargs)) + log.commands.debug('Calling {}'.format( + debug.format_call(self.handler, posargs, kwargs))) self.handler(*posargs, **kwargs) diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index aa7641143..1716604f8 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -208,6 +208,18 @@ def signal_name(sig): return m.group(1) +def _format_args(args=None, kwargs=None): + """Format a list of arguments/kwargs to a function-call like string.""" + if args is not None: + arglist = [utils.compact_text(repr(arg), 50) for arg in args] + else: + arglist = [] + if kwargs is not None: + for k, v in kwargs.items(): + arglist.append('{}={}'.format(k, utils.compact_text(repr(v), 50))) + return ', '.join(arglist) + + def dbg_signal(sig, args): """Get a string representation of a signal for debugging. @@ -218,6 +230,26 @@ def dbg_signal(sig, args): Return: A human-readable string representation of signal/args. """ - argstr = ', '.join([utils.elide(str(a).replace('\n', ' '), 20) - for a in args]) - return '{}({})'.format(signal_name(sig), argstr) + return '{}({})'.format(signal_name(sig), _format_args(args)) + + +def format_call(func, args=None, kwargs=None, full=True): + """Get a string representation of a function calls with the given args. + + Args: + func: The callable to print. + args: A list of positional arguments. + kwargs: A dict of named arguments. + full: Whether to print the full name + + Return: + A string with the function call. + """ + if full: + if func.__module__ is not None: + name = '.'.join([func.__module__, func.__qualname__]) + else: + name = func.__qualname__ + else: + name = func.__name__ + return '{}({})'.format(name, _format_args(args, kwargs)) diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py index 82ef05117..933c14ad9 100644 --- a/qutebrowser/utils/utilcmds.py +++ b/qutebrowser/utils/utilcmds.py @@ -19,10 +19,14 @@ """Misc. utility commands exposed to the user.""" + +from PyQt5.QtCore import pyqtRemoveInputHook, QCoreApplication + from functools import partial -from qutebrowser.utils import usertypes +from qutebrowser.utils import usertypes, log from qutebrowser.commands import runners, cmdexc, cmdutils +from qutebrowser.config import config, style _timers = [] @@ -57,3 +61,65 @@ def later(ms : int, *command : {'nargs': '+'}): timer.timeout.connect(partial(_commandrunner.run_safely, cmdline)) timer.timeout.connect(lambda: _timers.remove(timer)) timer.start() + + +@cmdutils.register(debug=True, name='debug-set-trace') +def set_trace(): + """Break into the debugger in the shell. + + // + + Based on http://stackoverflow.com/a/1745965/2085149 + """ + if sys.stdout is not None: + sys.stdout.flush() + print() + print("When done debugging, remember to execute:") + print(" from PyQt5 import QtCore; QtCore.pyqtRestoreInputHook()") + print("before executing c(ontinue).") + pyqtRemoveInputHook() + pdb.set_trace() + + +@cmdutils.register(debug=True) +def debug_crash(typ : ('exception', 'segfault') = 'exception'): + """Crash for debugging purposes. + + Args: + typ: either 'exception' or 'segfault'. + + Raises: + raises Exception when typ is not segfault. + segfaults when typ is (you don't say...) + """ + if typ == 'segfault': + # From python's Lib/test/crashers/bogus_code_obj.py + co = types.CodeType(0, 0, 0, 0, 0, b'\x04\x71\x00\x00', (), (), (), + '', '', 1, b'') + exec(co) # pylint: disable=exec-used + raise Exception("Segfault failed (wat.)") + else: + raise Exception("Forced crash") + + +@cmdutils.register(debug=True) +def debug_all_widgets(): + """Print a list of all widgets to debug log.""" + s = QCoreApplication.instance().get_all_widgets() + log.misc.debug(s) + + +@cmdutils.register(debug=True) +def debug_all_objects(): + """Print a list of all objects to the debug log.""" + s = QCoreApplication.instance().get_all_objects() + log.misc.debug(s) + + +@cmdutils.register(debug=True) +def debug_cache_stats(): + """Print LRU cache stats.""" + config_info = config.instance().get.cache_info() + style_info = style.get_stylesheet.cache_info() + log.misc.debug('config: {}'.format(config_info)) + log.misc.debug('style: {}'.format(style_info)) From 57d51ad9bb88f05dced3c47a68688bfebc90e697 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 Sep 2014 10:47:27 +0200 Subject: [PATCH 03/89] Lots of fixes for new command system. Squashed commit: - Fix getting current URL - Get rid of *args for hints. - Make enums work. - Fix moving commands to utilcmds. - Fix enums in argparse - Fix arg splitting for hints. - Fix default enum args. - Fix argument splitting for hints if None is given. - Fix set_cmd_text with flags and fix {url}. - Fix unittests - Fix tuple types for arguments. - Fix scroll-page. - Fix lint - Fix open_target. - Others --- .flake8 | 3 +- qutebrowser/browser/commands.py | 23 ++--- qutebrowser/browser/hints.py | 22 +++-- qutebrowser/commands/argparser.py | 50 ++++++++++- qutebrowser/commands/cmdexc.py | 7 ++ qutebrowser/commands/cmdutils.py | 110 +++++++++++++---------- qutebrowser/commands/command.py | 18 +++- qutebrowser/commands/runners.py | 36 +++++--- qutebrowser/config/configdata.py | 14 +-- qutebrowser/test/utils/test_debug.py | 6 +- qutebrowser/test/utils/test_utils.py | 21 +++++ qutebrowser/utils/debug.py | 51 +---------- qutebrowser/utils/utilcmds.py | 30 ++----- qutebrowser/utils/utils.py | 9 ++ qutebrowser/widgets/statusbar/command.py | 15 +++- qutebrowser/widgets/tabbedbrowser.py | 8 +- qutebrowser/widgets/webview.py | 12 +-- 17 files changed, 258 insertions(+), 177 deletions(-) diff --git a/.flake8 b/.flake8 index 0c7c865d4..78c542d81 100644 --- a/.flake8 +++ b/.flake8 @@ -11,7 +11,8 @@ # E222: Multiple spaces after operator # F811: Redifiniton # W292: No newline at end of file +# E701: multiple statements on one line # E702: multiple statements on one line -ignore=E241,E265,F401,E501,F821,F841,E222,F811,W292,E702 +ignore=E241,E265,F401,E501,F821,F841,E222,F811,W292,E701,E702 max_complexity = 12 exclude = ez_setup.py diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 9006c0fbc..0a54db600 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -233,7 +233,7 @@ class CommandDispatcher: else: diag = QPrintDialog() diag.setAttribute(Qt.WA_DeleteOnClose) - diag.open(lambda: tab.print(printdiag.printer())) + diag.open(lambda: tab.print(diag.printer())) @cmdutils.register(instance='mainwindow.tabs.cmd') def back(self, count=1): @@ -257,7 +257,7 @@ class CommandDispatcher: @cmdutils.register(instance='mainwindow.tabs.cmd') def hint(self, group=webelem.Group.all, target=hints.Target.normal, - *args : {'nargs': '*'}): + args=None): """Start hinting. Args: @@ -286,7 +286,7 @@ class CommandDispatcher: link. - `spawn`: Spawn a command. - *args: Arguments for spawn/userscript/fill. + args: Arguments for spawn/userscript/fill. - With `spawn`: The executable and arguments to spawn. `{hint-url}` will get replaced by the selected @@ -301,7 +301,7 @@ class CommandDispatcher: if frame is None: raise cmdexc.CommandError("No frame focused!") widget.hintmanager.start(frame, self._tabs.current_url(), group, - target, *args) + target, args) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) def follow_hint(self): @@ -333,7 +333,7 @@ class CommandDispatcher: self._prevnext(prev=False, newtab=tab) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) - def scroll(self, dx : float, dy : float, count=1): + def scroll(self, dx: float, dy: float, count=1): """Scroll the current tab by 'count * dx/dy'. Args: @@ -348,8 +348,8 @@ class CommandDispatcher: self._current_widget().page().currentFrame().scroll(dx, dy) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) - def scroll_perc(self, perc : float = None, - horizontal : {'flag': 'x'} = False, count=None): + def scroll_perc(self, perc: float=None, + horizontal: {'flag': 'x'}=False, count=None): """Scroll to a specific percentage of the page. The percentage can be given either as argument or as count. @@ -364,7 +364,7 @@ class CommandDispatcher: Qt.Horizontal if horizontal else Qt.Vertical) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) - def scroll_page(self, x : int, y : int, count=1): + def scroll_page(self, x: float, y: float, count=1): """Scroll the frame page-wise. Args: @@ -402,7 +402,8 @@ class CommandDispatcher: target = "clipboard" log.misc.debug("Yanking to {}: '{}'".format(target, s)) clipboard.setText(s, mode) - message.info("URL yanked to {}".format(target)) + what = 'Title' if title else 'URL' + message.info("{} yanked to {}".format(what, target)) @cmdutils.register(instance='mainwindow.tabs.cmd') def zoom_in(self, count=1): @@ -518,7 +519,7 @@ class CommandDispatcher: widget.openurl(url) @cmdutils.register(instance='mainwindow.tabs.cmd') - def tab_focus(self, index : int = None, count=None): + def tab_focus(self, index: (int, 'last')=None, count=None): """Select the tab given as argument/[count]. Args: @@ -542,7 +543,7 @@ class CommandDispatcher: idx)) @cmdutils.register(instance='mainwindow.tabs.cmd') - def tab_move(self, direction : ('+', '-') = None, count=None): + def tab_move(self, direction: ('+', '-')=None, count=None): """Move the current tab. Args: diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 1583fa331..f4676f337 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -20,6 +20,7 @@ """A HintManager to draw hints over links.""" import math +import shlex import subprocess import collections @@ -472,7 +473,7 @@ class HintManager(QObject): self.openurl.emit(url, newtab) def start(self, mainframe, baseurl, group=webelem.Group.all, - target=Target.normal, *args): + target=Target.normal, args=None): """Start hinting. Args: @@ -480,7 +481,7 @@ class HintManager(QObject): baseurl: URL of the current page. group: Which group of elements to hint. target: What to do with the link. See attribute docstring. - *args: Arguments for userscript/download + args: Arguments for userscript/download Emit: hint_strings_updated: Emitted to update keypraser. @@ -493,14 +494,13 @@ class HintManager(QObject): # on_mode_left, we are extra careful here. raise ValueError("start() was called with frame=None") if target in (Target.userscript, Target.spawn, Target.fill): - if not args: + if args is None: raise cmdexc.CommandError( - "Additional arguments are required with target " - "userscript/spawn/fill.") + "'args' is required with target userscript/spawn/fill.") else: - if args: + if args is not None: raise cmdexc.CommandError( - "Arguments are only allowed with target userscript/spawn.") + "'args' is only allowed with target userscript/spawn.") elems = [] ctx = HintContext() ctx.frames = webelem.get_child_frames(mainframe) @@ -514,7 +514,13 @@ class HintManager(QObject): raise cmdexc.CommandError("No elements found.") ctx.target = target ctx.baseurl = baseurl - ctx.args = args + if args is None: + ctx.args = None + else: + try: + ctx.args = shlex.split(args) + except ValueError as e: + raise cmdexc.CommandError("Could not split args: {}".format(e)) message.instance().set_text(self.HINT_TEXTS[target]) strings = self._hint_strings(visible_elems) for e, string in zip(visible_elems, strings): diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index a31f0988d..69ef61920 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -19,8 +19,12 @@ """argparse.ArgumentParser subclass to parse qutebrowser commands.""" + import argparse +from qutebrowser.commands import cmdexc +from qutebrowser.utils import utils + class ArgumentParserError(Exception): @@ -29,6 +33,8 @@ class ArgumentParserError(Exception): class ArgumentParser(argparse.ArgumentParser): + """Subclass ArgumentParser to be more suitable for runtime parsing.""" + def __init__(self): super().__init__(add_help=False) @@ -37,4 +43,46 @@ class ArgumentParser(argparse.ArgumentParser): 'Status: {}, message: {}'.format(status, msg)) def error(self, msg): - raise ArgumentParserError(msg) + raise ArgumentParserError(msg[0].upper() + msg[1:]) + + +def enum_getter(enum): + """Function factory to get an enum getter.""" + + def _get_enum_item(key): + """Helper function to get an enum item. + + Passes through existing items unmodified. + """ + if isinstance(key, enum): + return key + try: + return enum[key.replace('-', '_')] + except KeyError: + raise cmdexc.ArgumentTypeError("Invalid value {}.".format(key)) + + return _get_enum_item + + +def multitype_conv(tpl): + """Function factory to get a type converter for a choice of types.""" + + def _convert(value): + """Convert a value according to an iterable of possible arg types.""" + for typ in tpl: + if isinstance(typ, str): + if value == typ: + return value + elif utils.is_enum(typ): + return enum_getter(typ)(value) + elif callable(typ): + # int, float, etc. + if isinstance(value, typ): + return value + try: + return typ(value) + except ValueError: + pass + raise cmdexc.ArgumentTypeError('Invalid value {}.'.format(value)) + + return _convert diff --git a/qutebrowser/commands/cmdexc.py b/qutebrowser/commands/cmdexc.py index c5dd7f214..82334f919 100644 --- a/qutebrowser/commands/cmdexc.py +++ b/qutebrowser/commands/cmdexc.py @@ -49,6 +49,13 @@ class ArgumentCountError(CommandMetaError): pass +class ArgumentTypeError(CommandMetaError): + + """Raised when an argument had an invalid type.""" + + pass + + class PrerequisitesError(CommandMetaError): """Raised when a cmd can't be used because some prerequisites aren't met. diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 7302fbfec..818b43a3c 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -23,11 +23,11 @@ Module attributes: cmd_dict: A mapping from command-strings to command objects. """ -import enum import inspect import collections -from qutebrowser.utils import usertypes, qtutils, log, debug +from qutebrowser.utils import usertypes, qtutils, log, utils +from qutebrowser.utils import debug as debugutils from qutebrowser.commands import command, cmdexc, argparser cmd_dict = {} @@ -160,15 +160,16 @@ class register: # pylint: disable=invalid-name """ names = self._get_names(func) log.commands.vdebug("Registering command {}".format(names[0])) - if any(name in cmd_dict for name in names): - raise ValueError("{} is already registered!".format(name)) - has_count, desc, parser = self._inspect_func(func) + for name in names: + if name in cmd_dict: + raise ValueError("{} is already registered!".format(name)) + has_count, desc, parser, type_conv = self._inspect_func(func) cmd = command.Command( name=names[0], split=self.split, hide=self.hide, count=has_count, desc=desc, instance=self.instance, handler=func, completion=self.completion, modes=self.modes, - not_modes=self.not_modes, needs_js=self.needs_js, debug=self.debug, - parser=parser) + not_modes=self.not_modes, needs_js=self.needs_js, + is_debug=self.debug, parser=parser, type_conv=type_conv) for name in names: cmd_dict[name] = cmd return func @@ -202,15 +203,17 @@ class register: # pylint: disable=invalid-name func: The function to look at. Return: - A (has_count, desc, parser) tuple. + A (has_count, desc, parser, type_conv) tuple. has_count: Whether the command supports a count. desc: The description of the command. parser: The ArgumentParser to use when parsing the commandline. + type_conv: A mapping of args to type converter callables. """ + type_conv = {} signature = inspect.signature(func) if 'self' in signature.parameters and self.instance is None: raise ValueError("{} is a class method, but instance was not " - "given!".format(mainname)) + "given!".format(self.name[0])) has_count = 'count' in signature.parameters parser = argparser.ArgumentParser() if func.__doc__ is not None: @@ -224,43 +227,64 @@ class register: # pylint: disable=invalid-name args = [] kwargs = {} annotation_info = self._parse_annotation(param) - if annotation_info.typ is not None: - typ = annotation_info.typ - else: - typ = self._infer_type(param) - kwargs.update(self._type_to_argparse(typ)) + kwargs.update(self._param_to_argparse_kw( + param, annotation_info)) kwargs.update(annotation_info.kwargs) - if (param.kind == inspect.Parameter.VAR_POSITIONAL and - 'nargs' not in kwargs): # annotation_info overrides it - kwargs['nargs'] = '*' - is_flag = typ == bool - args += self._get_argparse_args(param, annotation_info, - is_flag) - callsig = debug.format_call(parser.add_argument, args, - kwargs, full=False) + args += self._param_to_argparse_pos(param, annotation_info) + typ = self._get_type(param, annotation_info) + if utils.is_enum(typ): + type_conv[param.name] = argparser.enum_getter(typ) + elif isinstance(typ, tuple): + type_conv[param.name] = argparser.multitype_conv(typ) + callsig = debugutils.format_call(parser.add_argument, args, + kwargs, full=False) log.commands.vdebug('Adding arg {} of type {} -> {}'.format( param.name, typ, callsig)) parser.add_argument(*args, **kwargs) - return has_count, desc, parser + return has_count, desc, parser, type_conv - def _get_argparse_args(self, param, annotation_info, is_flag): + def _param_to_argparse_pos(self, param, annotation_info): """Get a list of positional argparse arguments. Args: param: The inspect.Parameter instance for the current parameter. annotation_info: An AnnotationInfo tuple for the parameter. - is_flag: Whether the option is a flag or not. """ args = [] name = annotation_info.name or param.name shortname = annotation_info.flag or param.name[0] - if is_flag: + if self._get_type(param, annotation_info) == bool: args.append('--{}'.format(name)) args.append('-{}'.format(shortname)) else: args.append(name) return args + def _param_to_argparse_kw(self, param, annotation_info): + """Get argparse keyword arguments for a parameter. + + Args: + param: The inspect.Parameter object to get the args for. + annotation_info: An AnnotationInfo tuple for the parameter. + """ + kwargs = {} + typ = self._get_type(param, annotation_info) + if isinstance(typ, tuple): + pass + elif utils.is_enum(typ): + kwargs['choices'] = [e.name.replace('_', '-') for e in typ] + elif typ is bool: + kwargs['action'] = 'store_true' + elif typ is not None: + kwargs['type'] = typ + + if param.kind == inspect.Parameter.VAR_POSITIONAL: + kwargs['nargs'] = '*' + elif typ is not bool and param.default is not inspect.Parameter.empty: + kwargs['default'] = param.default + kwargs['nargs'] = '?' + return kwargs + def _parse_annotation(self, param): """Get argparse arguments and type from a parameter annotation. @@ -276,8 +300,9 @@ class register: # pylint: disable=invalid-name name: The long name if overridden. """ info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None} - log.commands.vdebug("Parsing annotation {}".format(param.annotation)) if param.annotation is not inspect.Parameter.empty: + log.commands.vdebug("Parsing annotation {}".format( + param.annotation)) if isinstance(param.annotation, dict): for field in ('type', 'flag', 'name'): if field in param.annotation: @@ -288,27 +313,16 @@ class register: # pylint: disable=invalid-name info['typ'] = param.annotation return self.AnnotationInfo(**info) - def _infer_type(self, param): - """Get the type of an argument from its default value.""" - if param.default is None or param.default is inspect.Parameter.empty: + def _get_type(self, param, annotation_info): + """Get the type of an argument from its default value or annotation. + + Args: + param: The inspect.Parameter to look at. + annotation_info: An AnnotationInfo tuple which overrides the type. + """ + if annotation_info.typ is not None: + return annotation_info.typ + elif param.default is None or param.default is inspect.Parameter.empty: return None else: return type(param.default) - - def _type_to_argparse(self, typ): - """Get argparse keyword arguments based on a type.""" - kwargs = {} - try: - is_enum = issubclass(typ, enum.Enum) - except TypeError: - is_enum = False - if isinstance(typ, tuple): - kwargs['choices'] = typ - elif is_enum: - kwargs['choices'] = [e.name.replace('_', '-') for e in typ] - elif typ is bool: - kwargs['action'] = 'store_true' - elif typ is not None: - kwargs['type'] = typ - - return kwargs diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index afb3b30e7..72af36f37 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -43,6 +43,7 @@ class Command: needs_js: Whether the command needs javascript enabled debug: Whether this is a debugging command (only shown with --debug). parser: The ArgumentParser to use to parse this command. + type_conv: A mapping of conversion functions for arguments. """ # TODO: @@ -50,7 +51,8 @@ class Command: # this might be combined with help texts or so as well def __init__(self, name, split, hide, count, desc, instance, handler, - completion, modes, not_modes, needs_js, debug, parser): + completion, modes, not_modes, needs_js, is_debug, parser, + type_conv): # I really don't know how to solve this in a better way, I tried. # pylint: disable=too-many-arguments self.name = name @@ -64,8 +66,9 @@ class Command: self.modes = modes self.not_modes = not_modes self.needs_js = needs_js - self.debug = debug + self.debug = is_debug self.parser = parser + self.type_conv = type_conv def _check_prerequisites(self): """Check if the command is permitted to run currently. @@ -114,7 +117,7 @@ class Command: try: namespace = self.parser.parse_args(args) except argparser.ArgumentParserError as e: - message.error(str(e)) + message.error('{}: {}'.format(self.name, e)) return for name, arg in vars(namespace).items(): @@ -124,6 +127,12 @@ class Command: # FIXME: This approach is rather naive, but for now it works. posargs += arg else: + if name in self.type_conv: + # We convert enum types after getting the values from + # argparse, because argparse's choices argument is + # processed after type conversation, which is not what we + # want. + arg = self.type_conv[name](arg) kwargs[name] = arg if self.instance is not None: @@ -140,4 +149,7 @@ class Command: self._check_prerequisites() log.commands.debug('Calling {}'.format( debug.format_call(self.handler, posargs, kwargs))) + # FIXME this won't work properly if some arguments are required to be + # positional, e.g.: + # def fun(one=True, two=False, *args) self.handler(*posargs, **kwargs) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 7ad83ed9d..02ac4477e 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -27,6 +27,20 @@ from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.utils import message, log, utils +def replace_variables(arglist): + """Utility function to replace variables like {url} in a list of args.""" + args = [] + for arg in arglist: + if arg == '{url}': + app = QCoreApplication.instance() + url = app.mainwindow.tabs.current_url().toString( + QUrl.FullyEncoded | QUrl.RemovePassword) + args.append(url) + else: + args.append(arg) + return args + + class SearchRunner(QObject): """Run searches on webpages. @@ -219,6 +233,14 @@ class CommandRunner: else: raise cmdexc.NoSuchCommandError( '{}: no such command'.format(cmdstr)) + self._split_args(argstr) + retargs = self._args[:] + if text.endswith(' '): + retargs.append('') + return [cmdstr] + retargs + + def _split_args(self, argstr): + """Split the arguments from an arg string.""" if argstr is None: self._args = [] elif self._cmd.split: @@ -244,10 +266,6 @@ class CommandRunner: # If there are only flags, we got it right on the first try # already. self._args = split_args - retargs = self._args[:] - if text.endswith(' '): - retargs.append('') - return [cmdstr] + retargs def run(self, text, count=None): """Parse a command from a line of text and run it. @@ -261,15 +279,11 @@ class CommandRunner: self.run(sub, count) return self.parse(text) - app = QCoreApplication.instance() - cur_url = app.mainwindow.tabs.current_url().toString( - QUrl.FullyEncoded | QUrl.RemovePassword) - self._args = [cur_url if e == '{url}' else e for e in self._args] + args = replace_variables(self._args) if count is not None: - self._cmd.run(self._args, count=count) + self._cmd.run(args, count=count) else: - self._cmd.run(self._args) - + self._cmd.run(args) @pyqtSlot(str, int) def run_safely(self, text, count=None): diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 7afff556c..263c6cb8e 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -584,11 +584,11 @@ DATA = collections.OrderedDict([ ('keybind', sect.ValueList( typ.KeyBindingName(), typ.KeyBinding(), ('o', 'set-cmd-text ":open "'), - ('go', 'set-cmd-text :open {url}'), + ('go', 'set-cmd-text ":open {url}"'), ('O', 'set-cmd-text ":open -t "'), - ('gO', 'set-cmd-text :open -t {url}'), + ('gO', 'set-cmd-text ":open -t {url}"'), ('xo', 'set-cmd-text ":open -b "'), - ('xO', 'set-cmd-text :open -b {url}'), + ('xO', 'set-cmd-text ":open -b {url}"'), ('ga', 'open -t about:blank'), ('d', 'tab-close'), ('co', 'tab-only'), @@ -607,9 +607,9 @@ DATA = collections.OrderedDict([ (';i', 'hint images'), (';I', 'hint images tab'), ('.i', 'hint images tab-bg'), - (';o', 'hint links fill :open {hint-url}'), - (';O', 'hint links fill :open -t {hint-url}'), - ('.o', 'hint links fill :open -b {hint-url}'), + (';o', 'hint links fill ":open {hint-url}"'), + (';O', 'hint links fill ":open -t {hint-url}"'), + ('.o', 'hint links fill ":open -b {hint-url}"'), (';y', 'hint links yank'), (';Y', 'hint links yank-primary'), (';r', 'hint links rapid'), @@ -637,7 +637,7 @@ DATA = collections.OrderedDict([ ('B', 'set-cmd-text ":quickmark-load -t "'), ('sf', 'save'), ('ss', 'set-cmd-text ":set "'), - ('sl', 'set-cmd-text ":set -t"'), + ('sl', 'set-cmd-text ":set -t "'), ('sk', 'set-cmd-text ":set keybind "'), ('-', 'zoom-out'), ('+', 'zoom-in'), diff --git a/qutebrowser/test/utils/test_debug.py b/qutebrowser/test/utils/test_debug.py index 6f9e70a05..5d743595a 100644 --- a/qutebrowser/test/utils/test_debug.py +++ b/qutebrowser/test/utils/test_debug.py @@ -137,13 +137,13 @@ class TestDebug(unittest.TestCase): def test_dbg_signal_eliding(self): """Test eliding in dbg_signal().""" self.assertEqual(debug.dbg_signal(self.signal, - [12345678901234567890123]), - 'fake(1234567890123456789\u2026)') + ['x' * 201]), + "fake('{}\u2026)".format('x' * 198)) def test_dbg_signal_newline(self): """Test dbg_signal() with a newline.""" self.assertEqual(debug.dbg_signal(self.signal, ['foo\nbar']), - 'fake(foo bar)') + r"fake('foo\nbar')") if __name__ == '__main__': diff --git a/qutebrowser/test/utils/test_utils.py b/qutebrowser/test/utils/test_utils.py index 38ad74468..f7d915000 100644 --- a/qutebrowser/test/utils/test_utils.py +++ b/qutebrowser/test/utils/test_utils.py @@ -21,6 +21,7 @@ import os import sys +import enum import shutil import unittest import os.path @@ -511,5 +512,25 @@ class NormalizeTests(unittest.TestCase): self.assertEqual(utils.normalize_keystr(orig), repl) +class IsEnumTests(unittest.TestCase): + + """Test is_enum.""" + + def test_enum(self): + """Test is_enum with an enum.""" + e = enum.Enum('Foo', 'bar, baz') + self.assertTrue(utils.is_enum(e)) + + def test_class(self): + """Test is_enum with a non-enum class.""" + # pylint: disable=multiple-statements,missing-docstring + class Test: pass + self.assertFalse(utils.is_enum(Test)) + + def test_object(self): + """Test is_enum with a non-enum object.""" + self.assertFalse(utils.is_enum(23)) + + if __name__ == '__main__': unittest.main() diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index 1716604f8..db0f6d323 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -21,58 +21,11 @@ import re import sys -import types import functools from PyQt5.QtCore import QEvent, QCoreApplication from qutebrowser.utils import log, utils -from qutebrowser.commands import cmdutils -from qutebrowser.config import config, style - - -@cmdutils.register(debug=True) -def debug_crash(typ : ('exception', 'segfault') = 'exception'): - """Crash for debugging purposes. - - Args: - typ: either 'exception' or 'segfault'. - - Raises: - raises Exception when typ is not segfault. - segfaults when typ is (you don't say...) - """ - if typ == 'segfault': - # From python's Lib/test/crashers/bogus_code_obj.py - co = types.CodeType(0, 0, 0, 0, 0, b'\x04\x71\x00\x00', (), (), (), - '', '', 1, b'') - exec(co) # pylint: disable=exec-used - raise Exception("Segfault failed (wat.)") - else: - raise Exception("Forced crash") - - -@cmdutils.register(debug=True) -def debug_all_widgets(): - """Print a list of all widgets to debug log.""" - s = QCoreApplication.instance().get_all_widgets() - log.misc.debug(s) - - -@cmdutils.register(debug=True) -def debug_all_objects(): - """Print a list of all objects to the debug log.""" - s = QCoreApplication.instance().get_all_objects() - log.misc.debug(s) - - -@cmdutils.register(debug=True) -def debug_cache_stats(): - """Print LRU cache stats.""" - config_info = config.instance().get.cache_info() - style_info = style.get_stylesheet.cache_info() - log.misc.debug('config: {}'.format(config_info)) - log.misc.debug('style: {}'.format(style_info)) def log_events(klass): @@ -211,12 +164,12 @@ def signal_name(sig): def _format_args(args=None, kwargs=None): """Format a list of arguments/kwargs to a function-call like string.""" if args is not None: - arglist = [utils.compact_text(repr(arg), 50) for arg in args] + arglist = [utils.compact_text(repr(arg), 200) for arg in args] else: arglist = [] if kwargs is not None: for k, v in kwargs.items(): - arglist.append('{}={}'.format(k, utils.compact_text(repr(v), 50))) + arglist.append('{}={}'.format(k, utils.compact_text(repr(v), 200))) return ', '.join(arglist) diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py index 933c14ad9..4fd1435ce 100644 --- a/qutebrowser/utils/utilcmds.py +++ b/qutebrowser/utils/utilcmds.py @@ -19,10 +19,11 @@ """Misc. utility commands exposed to the user.""" +import types +import functools -from PyQt5.QtCore import pyqtRemoveInputHook, QCoreApplication -from functools import partial +from PyQt5.QtCore import QCoreApplication from qutebrowser.utils import usertypes, log from qutebrowser.commands import runners, cmdexc, cmdutils @@ -40,7 +41,7 @@ def init(): @cmdutils.register() -def later(ms : int, *command : {'nargs': '+'}): +def later(ms: int, *command: {'nargs': '+'}): """Execute a command after some time. Args: @@ -58,31 +59,14 @@ def later(ms : int, *command : {'nargs': '+'}): "int representation.") _timers.append(timer) cmdline = ' '.join(command) - timer.timeout.connect(partial(_commandrunner.run_safely, cmdline)) + timer.timeout.connect(functools.partial( + _commandrunner.run_safely, cmdline)) timer.timeout.connect(lambda: _timers.remove(timer)) timer.start() -@cmdutils.register(debug=True, name='debug-set-trace') -def set_trace(): - """Break into the debugger in the shell. - - // - - Based on http://stackoverflow.com/a/1745965/2085149 - """ - if sys.stdout is not None: - sys.stdout.flush() - print() - print("When done debugging, remember to execute:") - print(" from PyQt5 import QtCore; QtCore.pyqtRestoreInputHook()") - print("before executing c(ontinue).") - pyqtRemoveInputHook() - pdb.set_trace() - - @cmdutils.register(debug=True) -def debug_crash(typ : ('exception', 'segfault') = 'exception'): +def debug_crash(typ: ('exception', 'segfault')='exception'): """Crash for debugging purposes. Args: diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index c790e6410..35afbed19 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -22,6 +22,7 @@ import os import io import sys +import enum import shlex import os.path import urllib.request @@ -494,3 +495,11 @@ def disabled_excepthook(): # unchanged. Otherwise, we reset it. if sys.excepthook is sys.__excepthook__: sys.excepthook = old_excepthook + + +def is_enum(obj): + """Check if a given object is an enum.""" + try: + return issubclass(obj, enum.Enum) + except TypeError: + return False diff --git a/qutebrowser/widgets/statusbar/command.py b/qutebrowser/widgets/statusbar/command.py index d2d8363c7..3ff65ee64 100644 --- a/qutebrowser/widgets/statusbar/command.py +++ b/qutebrowser/widgets/statusbar/command.py @@ -19,7 +19,7 @@ """The commandline in the statusbar.""" -from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QCoreApplication, QUrl from PyQt5.QtWidgets import QSizePolicy, QApplication from qutebrowser.keyinput import modeman, modeparsers @@ -161,7 +161,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): self.show_cmd.emit() @cmdutils.register(instance='mainwindow.status.cmd', name='set-cmd-text') - def set_cmd_text_command(self, *strings): + def set_cmd_text_command(self, text): """Preset the statusbar to some text. // @@ -170,9 +170,16 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): strings which will get joined. Args: - strings: A list of strings to set. + text: The commandline to set. """ - text = ' '.join(strings) + app = QCoreApplication.instance() + url = app.mainwindow.tabs.current_url().toString( + QUrl.FullyEncoded | QUrl.RemovePassword) + # FIXME we currently replace the URL in any place in the arguments, + # rather than just replacing it if it is a dedicated argument. We could + # split the args, but then trailing spaces would be lost, so I'm not + # sure what's the best thing to do here + text = text.replace('{url}', url) if not text[0] in modeparsers.STARTCHARS: raise cmdexc.CommandError( "Invalid command text '{}'.".format(text)) diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index 43bc9ebd8..08d179d17 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -22,7 +22,7 @@ import functools from PyQt5.QtWidgets import QSizePolicy -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer, QUrl from PyQt5.QtGui import QIcon from PyQt5.QtWebKitWidgets import QWebPage @@ -205,7 +205,11 @@ class TabbedBrowser(tabwidget.TabWidget): Raise: CommandError if the current URL is invalid. """ - url = self.currentWidget().cur_url + widget = self.currentWidget() + if widget is None: + url = QUrl() + else: + url = widget.cur_url try: qtutils.ensure_valid(url) except qtutils.QtValueError as e: diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index b94d74759..34b8f3016 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -221,20 +221,20 @@ class WebView(QWebView): e: The QMouseEvent. """ if self._force_open_target is not None: - self._open_target = self._force_open_target + self.open_target = self._force_open_target self._force_open_target = None log.mouse.debug("Setting force target: {}".format( - self._open_target)) + self.open_target)) elif (e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier): if config.get('tabs', 'background-tabs'): - self._open_target = usertypes.ClickTarget.tab_bg + self.open_target = usertypes.ClickTarget.tab_bg else: - self._open_target = usertypes.ClickTarget.tab + self.open_target = usertypes.ClickTarget.tab log.mouse.debug("Middle click, setting target: {}".format( - self._open_target)) + self.open_target)) else: - self._open_target = usertypes.ClickTarget.normal + self.open_target = usertypes.ClickTarget.normal log.mouse.debug("Normal click, setting normal target") def shutdown(self): From 03a0a1c5992131249676dbb18f0d6e428857dadf Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 4 Sep 2014 17:58:28 +0200 Subject: [PATCH 04/89] commands: Handle ArgumentParser exit. --- qutebrowser/commands/argparser.py | 12 ++++++++++-- qutebrowser/commands/command.py | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index 69ef61920..2aa704e1a 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -31,6 +31,15 @@ class ArgumentParserError(Exception): """Exception raised when the ArgumentParser signals an error.""" +class ArgumentParserExit(Exception): + + """Exception raised when the argument parser exitted.""" + + def __init__(self, status, msg): + self.status = status + super().__init__(msg) + + class ArgumentParser(argparse.ArgumentParser): """Subclass ArgumentParser to be more suitable for runtime parsing.""" @@ -39,8 +48,7 @@ class ArgumentParser(argparse.ArgumentParser): super().__init__(add_help=False) def exit(self, status=0, msg=None): - raise AssertionError('exit called, this should never happen. ' - 'Status: {}, message: {}'.format(status, msg)) + raise ArgumentParserExit(status, msg) def error(self, msg): raise ArgumentParserError(msg[0].upper() + msg[1:]) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 72af36f37..58b3b9911 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -119,6 +119,10 @@ class Command: except argparser.ArgumentParserError as e: message.error('{}: {}'.format(self.name, e)) return + except argparser.ArgumentParserExit as e: + log.commands.debug("argparser exited with status {}: {}".format( + e.status, e)) + return for name, arg in vars(namespace).items(): if isinstance(arg, list): From 32e24479b973553a87ab122f4d3a5a6f527d9b3f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 4 Sep 2014 17:59:29 +0200 Subject: [PATCH 05/89] commands.cmdutils: Clean up decorator. --- qutebrowser/commands/cmdutils.py | 40 ++++++++++++++------------------ 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 818b43a3c..85e6480c3 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -135,6 +135,8 @@ class register: # pylint: disable=invalid-name self.needs_js = needs_js self.debug = debug self.ignore_args = ignore_args + self.parser = None + self.func = None if modes is not None: for m in modes: if not isinstance(m, usertypes.KeyMode): @@ -158,23 +160,25 @@ class register: # pylint: disable=invalid-name Return: The original function (unmodified). """ - names = self._get_names(func) + self.func = func + names = self._get_names() log.commands.vdebug("Registering command {}".format(names[0])) for name in names: if name in cmd_dict: raise ValueError("{} is already registered!".format(name)) - has_count, desc, parser, type_conv = self._inspect_func(func) + self.parser = argparser.ArgumentParser(names[0]) + has_count, desc, type_conv = self._inspect_func() cmd = command.Command( name=names[0], split=self.split, hide=self.hide, count=has_count, desc=desc, instance=self.instance, handler=func, completion=self.completion, modes=self.modes, not_modes=self.not_modes, needs_js=self.needs_js, - is_debug=self.debug, parser=parser, type_conv=type_conv) + is_debug=self.debug, parser=self.parser, type_conv=type_conv) for name in names: cmd_dict[name] = cmd return func - def _get_names(self, func): + def _get_names(self): """Get the name(s) which should be used for the current command. If the name hasn't been overridden explicitely, the function name is @@ -183,41 +187,33 @@ class register: # pylint: disable=invalid-name If it has been set, it can either be a string which is used directly, or an iterable. - Args: - func: The function to get the name for. - Return: A list of names, with the main name being the first item. """ if self.name is None: - return [func.__name__.lower().replace('_', '-')] + return [self.func.__name__.lower().replace('_', '-')] elif isinstance(self.name, str): return [self.name] else: return self.name - def _inspect_func(self, func): - """Inspect a function to get useful informations from it. - - Args: - func: The function to look at. + def _inspect_func(self): + """Inspect the function to get useful informations from it. Return: A (has_count, desc, parser, type_conv) tuple. has_count: Whether the command supports a count. desc: The description of the command. - parser: The ArgumentParser to use when parsing the commandline. type_conv: A mapping of args to type converter callables. """ type_conv = {} - signature = inspect.signature(func) + signature = inspect.signature(self.func) if 'self' in signature.parameters and self.instance is None: raise ValueError("{} is a class method, but instance was not " "given!".format(self.name[0])) has_count = 'count' in signature.parameters - parser = argparser.ArgumentParser() - if func.__doc__ is not None: - desc = func.__doc__.splitlines()[0].strip() + if self.func.__doc__ is not None: + desc = self.func.__doc__.splitlines()[0].strip() else: desc = "" if not self.ignore_args: @@ -236,12 +232,12 @@ class register: # pylint: disable=invalid-name type_conv[param.name] = argparser.enum_getter(typ) elif isinstance(typ, tuple): type_conv[param.name] = argparser.multitype_conv(typ) - callsig = debugutils.format_call(parser.add_argument, args, - kwargs, full=False) + callsig = debugutils.format_call(self.parser.add_argument, + args, kwargs, full=False) log.commands.vdebug('Adding arg {} of type {} -> {}'.format( param.name, typ, callsig)) - parser.add_argument(*args, **kwargs) - return has_count, desc, parser, type_conv + self.parser.add_argument(*args, **kwargs) + return has_count, desc, type_conv def _param_to_argparse_pos(self, param, annotation_info): """Get a list of positional argparse arguments. From a656c8cfb0901bf840b47e8634009e2e96df4eda Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 4 Sep 2014 17:59:50 +0200 Subject: [PATCH 06/89] commands: Add initial --help argument support. --- qutebrowser/commands/argparser.py | 16 +++++++++++++++- qutebrowser/commands/cmdutils.py | 3 +++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index 2aa704e1a..4e43259a1 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -22,10 +22,15 @@ import argparse +from PyQt5.QtCore import QCoreApplication, QUrl + from qutebrowser.commands import cmdexc from qutebrowser.utils import utils +SUPPRESS = argparse.SUPPRESS + + class ArgumentParserError(Exception): """Exception raised when the ArgumentParser signals an error.""" @@ -40,11 +45,20 @@ class ArgumentParserExit(Exception): super().__init__(msg) +class HelpAction(argparse.Action): + + def __call__(self, parser, _namespace, _values, _option_string=None): + QCoreApplication.instance().mainwindow.tabs.tabopen( + QUrl('qute:help/commands.html#{}'.format(parser.name))) + parser.exit() + + class ArgumentParser(argparse.ArgumentParser): """Subclass ArgumentParser to be more suitable for runtime parsing.""" - def __init__(self): + def __init__(self, name): + self.name = name super().__init__(add_help=False) def exit(self, status=0, msg=None): diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 85e6480c3..4ecf9379a 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -167,6 +167,9 @@ class register: # pylint: disable=invalid-name if name in cmd_dict: raise ValueError("{} is already registered!".format(name)) self.parser = argparser.ArgumentParser(names[0]) + self.parser.add_argument('-h', '--help', action=argparser.HelpAction, + default=argparser.SUPPRESS, nargs=0, + help="Show this help message.") has_count, desc, type_conv = self._inspect_func() cmd = command.Command( name=names[0], split=self.split, hide=self.hide, count=has_count, From b5f28b6ff2d308ac787f5b7d16efbc482e7e8628 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Sep 2014 06:38:23 +0200 Subject: [PATCH 07/89] commands.argparser: Make ArgumentParser take args, add name. --- qutebrowser/commands/argparser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index 4e43259a1..ac7799481 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -57,9 +57,9 @@ class ArgumentParser(argparse.ArgumentParser): """Subclass ArgumentParser to be more suitable for runtime parsing.""" - def __init__(self, name): + def __init__(self, name, *args, **kwargs): self.name = name - super().__init__(add_help=False) + super().__init__(*args, add_help=False, prog=name, **kwargs) def exit(self, status=0, msg=None): raise ArgumentParserExit(status, msg) From b453ae563ed096fa8698758a9406517dcd754fbd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Sep 2014 06:38:57 +0200 Subject: [PATCH 08/89] Clean up docstring parsing and move it into qutebrowser for commands. --- qutebrowser/commands/cmdutils.py | 17 ++++-- qutebrowser/utils/utils.py | 93 +++++++++++++++++++++++++++++++- scripts/generate_doc.py | 84 +++-------------------------- 3 files changed, 113 insertions(+), 81 deletions(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 4ecf9379a..33cace1dc 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -137,6 +137,7 @@ class register: # pylint: disable=invalid-name self.ignore_args = ignore_args self.parser = None self.func = None + self.docparser = None if modes is not None: for m in modes: if not isinstance(m, usertypes.KeyMode): @@ -166,10 +167,13 @@ class register: # pylint: disable=invalid-name for name in names: if name in cmd_dict: raise ValueError("{} is already registered!".format(name)) - self.parser = argparser.ArgumentParser(names[0]) + self.docparser = utils.DocstringParser(func) + self.parser = argparser.ArgumentParser( + names[0], description=self.docparser.short_desc, + epilog=self.docparser.long_desc) self.parser.add_argument('-h', '--help', action=argparser.HelpAction, - default=argparser.SUPPRESS, nargs=0, - help="Show this help message.") + default=argparser.SUPPRESS, nargs=0, + help=argparser.SUPPRESS) has_count, desc, type_conv = self._inspect_func() cmd = command.Command( name=names[0], split=self.split, hide=self.hide, count=has_count, @@ -267,7 +271,13 @@ class register: # pylint: disable=invalid-name annotation_info: An AnnotationInfo tuple for the parameter. """ kwargs = {} + + try: + kwargs['help'] = self.docparser.arg_descs[param.name] + except KeyError: + pass typ = self._get_type(param, annotation_info) + if isinstance(typ, tuple): pass elif utils.is_enum(typ): @@ -282,6 +292,7 @@ class register: # pylint: disable=invalid-name elif typ is not bool and param.default is not inspect.Parameter.empty: kwargs['default'] = param.default kwargs['nargs'] = '?' + return kwargs def _parse_annotation(self, param): diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 35afbed19..b5af9e798 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -21,9 +21,11 @@ import os import io +import re import sys import enum import shlex +import inspect import os.path import urllib.request import urllib.parse @@ -36,7 +38,7 @@ from PyQt5.QtGui import QKeySequence, QColor import pkg_resources import qutebrowser -from qutebrowser.utils import qtutils +from qutebrowser.utils import usertypes, qtutils def elide(text, length): @@ -503,3 +505,92 @@ def is_enum(obj): return issubclass(obj, enum.Enum) except TypeError: return False + + +class DocstringParser: + + """Generate documentation based on a docstring of a command handler. + + The docstring needs to follow the format described in HACKING. + """ + + State = usertypes.enum('State', 'short', 'desc', 'desc_hidden', + 'arg_start', 'arg_inside', 'misc') + + def __init__(self, func): + """Constructor. + + Args: + func: The function to parse the docstring for. + """ + self.state = self.State.short + self.short_desc = [] + self.long_desc = [] + self.arg_descs = collections.OrderedDict() + self.cur_arg_name = None + self.handlers = { + self.State.short: self._parse_short, + self.State.desc: self._parse_desc, + self.State.desc_hidden: self._skip, + self.State.arg_start: self._parse_arg_start, + self.State.arg_inside: self._parse_arg_inside, + self.State.misc: self._skip, + } + doc = inspect.getdoc(func) + for line in doc.splitlines(): + handler = self.handlers[self.state] + stop = handler(line) + if stop: + break + for k, v in self.arg_descs.items(): + self.arg_descs[k] = ' '.join(v) + self.long_desc = ' '.join(self.long_desc) + self.short_desc = ' '.join(self.short_desc) + + def _process_arg(self, line): + """Helper method to process a line like 'fooarg: Blah blub'.""" + self.cur_arg_name, argdesc = line.split(':', maxsplit=1) + self.cur_arg_name = self.cur_arg_name.strip().lstrip('*') + self.arg_descs[self.cur_arg_name] = [argdesc.strip()] + + def _skip(self, line): + """Handler to ignore everything until we get 'Args:'.""" + if line.startswith('Args:'): + self.state = self.State.arg_start + + def _parse_short(self, line): + """Parse the short description (first block) in the docstring.""" + if not line: + self.state = self.State.desc + else: + self.short_desc.append(line.strip()) + + def _parse_desc(self, line): + """Parse the long description in the docstring.""" + if line.startswith('Args:'): + self.state = self.State.arg_start + elif line.startswith('Emit:') or line.startswith('Raise:'): + self.state = self.State.misc + elif line.strip() == '//': + self.state = self.State.desc_hidden + elif line.strip(): + self.long_desc.append(line.strip()) + + def _parse_arg_start(self, line): + """Parse first argument line.""" + self._process_arg(line) + self.state = self.State.arg_inside + + def _parse_arg_inside(self, line): + """Parse subsequent argument lines.""" + argname = self.cur_arg_name + if re.match(r'^[A-Z][a-z]+:$', line): + if not self.arg_descs[argname][-1].strip(): + self.arg_descs[argname] = self.arg_descs[argname][:-1] + return True + elif not line.strip(): + self.arg_descs[argname].append('\n\n') + elif line[4:].startswith(' '): + self.arg_descs[argname].append(line.strip() + '\n') + else: + self._process_arg(line) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 594dcb5f5..2cf07079a 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -20,7 +20,6 @@ """Generate asciidoc source for qutebrowser based on docstrings.""" -import re import os import sys import html @@ -38,7 +37,7 @@ import qutebrowser.app from qutebrowser import qutebrowser as qutequtebrowser from qutebrowser.commands import cmdutils from qutebrowser.config import configdata -from qutebrowser.utils import usertypes +from qutebrowser.utils import utils def _open_file(name, mode='w'): @@ -46,75 +45,6 @@ def _open_file(name, mode='w'): return open(name, mode, newline='\n', encoding='utf-8') -def _parse_docstring(func): # noqa - """Generate documentation based on a docstring of a command handler. - - The docstring needs to follow the format described in HACKING. - - Args: - func: The function to generate the docstring for. - - Return: - A (short_desc, long_desc, arg_descs) tuple. - """ - # pylint: disable=too-many-branches - State = usertypes.enum('State', 'short', # pylint: disable=invalid-name - 'desc', 'desc_hidden', 'arg_start', 'arg_inside', - 'misc') - doc = inspect.getdoc(func) - lines = doc.splitlines() - - cur_state = State.short - - short_desc = [] - long_desc = [] - arg_descs = collections.OrderedDict() - cur_arg_name = None - - for line in lines: - if cur_state == State.short: - if not line: - cur_state = State.desc - else: - short_desc.append(line.strip()) - elif cur_state == State.desc: - if line.startswith('Args:'): - cur_state = State.arg_start - elif line.startswith('Emit:') or line.startswith('Raise:'): - cur_state = State.misc - elif line.strip() == '//': - cur_state = State.desc_hidden - elif line.strip(): - long_desc.append(line.strip()) - elif cur_state == State.misc: - if line.startswith('Args:'): - cur_state = State.arg_start - else: - pass - elif cur_state == State.desc_hidden: - if line.startswith('Args:'): - cur_state = State.arg_start - elif cur_state == State.arg_start: - cur_arg_name, argdesc = line.split(':', maxsplit=1) - cur_arg_name = cur_arg_name.strip().lstrip('*') - arg_descs[cur_arg_name] = [argdesc.strip()] - cur_state = State.arg_inside - elif cur_state == State.arg_inside: - if re.match('^[A-Z][a-z]+:$', line): - if not arg_descs[cur_arg_name][-1].strip(): - arg_descs[cur_arg_name] = arg_descs[cur_arg_name][:-1] - break - elif not line.strip(): - arg_descs[cur_arg_name].append('\n\n') - elif line[4:].startswith(' '): - arg_descs[cur_arg_name].append(line.strip() + '\n') - else: - cur_arg_name, argdesc = line.split(':', maxsplit=1) - cur_arg_name = cur_arg_name.strip().lstrip('*') - arg_descs[cur_arg_name] = [argdesc.strip()] - return (short_desc, long_desc, arg_descs) - - def _get_cmd_syntax(name, cmd): """Get the command syntax for a command.""" words = [] @@ -179,14 +109,14 @@ def _get_command_doc(name, cmd): if syntax != name: output.append('Syntax: +:{}+'.format(syntax)) output.append("") - short_desc, long_desc, arg_descs = _parse_docstring(cmd.handler) - output.append(' '.join(short_desc)) + parser = utils.DocstringParser(cmd.handler) + output.append(parser.short_desc) output.append("") - output.append(' '.join(long_desc)) - if arg_descs: + output.append(parser.long_desc) + if parser.arg_descs: output.append("") - for arg, desc in arg_descs.items(): - text = ' '.join(desc).splitlines() + for arg, desc in parser.arg_descs.items(): + text = desc.splitlines() firstline = text[0].replace(', or None', '') item = "* +{}+: {}".format(arg, firstline) if arg in defaults: From 0a094c6e5820b38690b76444c53023161030e83e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Sep 2014 06:55:53 +0200 Subject: [PATCH 09/89] Refactor HintManager.start --- qutebrowser/browser/hints.py | 79 ++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index f4676f337..5d9ca88a9 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -453,6 +453,46 @@ class HintManager(QObject): f.contentsSizeChanged.connect(self.on_contents_size_changed) self._context.connected_frames.append(f) + def _check_args(self, target, args): + """Check the arguments passed to start() and raise if they're wrong. + + Args: + target: A Target enum member. + args: Arguments for userscript/download + """ + if not isinstance(target, Target): + raise TypeError("Target {} is no Target member!".format(target)) + if target in (Target.userscript, Target.spawn, Target.fill): + if args is None: + raise cmdexc.CommandError( + "'args' is required with target userscript/spawn/fill.") + else: + if args is not None: + raise cmdexc.CommandError( + "'args' is only allowed with target userscript/spawn.") + + def _init_elements(self, mainframe, group): + """Initialize the elements and labels based on the context set. + + Args: + mainframe: The main QWebFrame. + group: A Group enum member (which elements to find). + """ + elems = [] + for f in self._context.frames: + for e in f.findAllElements(webelem.SELECTORS[group]): + elems.append(webelem.WebElementWrapper(e)) + filterfunc = webelem.FILTERS.get(group, lambda e: True) + visible_elems = [e for e in elems if filterfunc(e) and + e.is_visible(mainframe)] + if not visible_elems: + raise cmdexc.CommandError("No elements found.") + strings = self._hint_strings(visible_elems) + for e, string in zip(visible_elems, strings): + label = self._draw_label(e, string) + self._context.elems[string] = ElemTuple(e, label) + self.hint_strings_updated.emit(strings) + def follow_prevnext(self, frame, baseurl, prev=False, newtab=False): """Click a "previous"/"next" element on the page. @@ -486,49 +526,26 @@ class HintManager(QObject): Emit: hint_strings_updated: Emitted to update keypraser. """ - if not isinstance(target, Target): - raise TypeError("Target {} is no Target member!".format(target)) + self._check_args(target, args) if mainframe is None: # This should never happen since we check frame before calling # start. But since we had a bug where frame is None in # on_mode_left, we are extra careful here. raise ValueError("start() was called with frame=None") - if target in (Target.userscript, Target.spawn, Target.fill): - if args is None: - raise cmdexc.CommandError( - "'args' is required with target userscript/spawn/fill.") - else: - if args is not None: - raise cmdexc.CommandError( - "'args' is only allowed with target userscript/spawn.") - elems = [] - ctx = HintContext() - ctx.frames = webelem.get_child_frames(mainframe) - for f in ctx.frames: - for e in f.findAllElements(webelem.SELECTORS[group]): - elems.append(webelem.WebElementWrapper(e)) - filterfunc = webelem.FILTERS.get(group, lambda e: True) - visible_elems = [e for e in elems if filterfunc(e) and - e.is_visible(mainframe)] - if not visible_elems: - raise cmdexc.CommandError("No elements found.") - ctx.target = target - ctx.baseurl = baseurl + self._context = HintContext() + self._context.target = target + self._context.baseurl = baseurl + self._context.frames = webelem.get_child_frames(mainframe) if args is None: - ctx.args = None + self._context.args = None else: try: - ctx.args = shlex.split(args) + self._context.args = shlex.split(args) except ValueError as e: raise cmdexc.CommandError("Could not split args: {}".format(e)) + self._init_elements(mainframe, group) message.instance().set_text(self.HINT_TEXTS[target]) - strings = self._hint_strings(visible_elems) - for e, string in zip(visible_elems, strings): - label = self._draw_label(e, string) - ctx.elems[string] = ElemTuple(e, label) - self._context = ctx self._connect_frame_signals() - self.hint_strings_updated.emit(strings) try: modeman.enter(usertypes.KeyMode.hint, 'HintManager.start') except modeman.ModeLockedError: From 05f3809d018d65f587d976b1c58e2361b1f476f6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Sep 2014 06:57:02 +0200 Subject: [PATCH 10/89] Fix lint --- qutebrowser/commands/argparser.py | 6 ++++++ qutebrowser/utils/debug.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index ac7799481..220c8be12 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -47,6 +47,12 @@ class ArgumentParserExit(Exception): class HelpAction(argparse.Action): + """Argparse action to open the help page in the browser. + + This is horrible encapsulation, but I can't think of a good way to do this + better... + """ + def __call__(self, parser, _namespace, _values, _option_string=None): QCoreApplication.instance().mainwindow.tabs.tabopen( QUrl('qute:help/commands.html#{}'.format(parser.name))) diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index db0f6d323..202d41e84 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -23,7 +23,7 @@ import re import sys import functools -from PyQt5.QtCore import QEvent, QCoreApplication +from PyQt5.QtCore import QEvent from qutebrowser.utils import log, utils From b03b0a173c34cda07656b4cbb658b995fe80c9d9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Sep 2014 07:12:35 +0200 Subject: [PATCH 11/89] generate_doc: Use argparse for command syntax --- scripts/generate_doc.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 2cf07079a..c430d4ff6 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -47,27 +47,10 @@ def _open_file(name, mode='w'): def _get_cmd_syntax(name, cmd): """Get the command syntax for a command.""" - words = [] - argspec = inspect.getfullargspec(cmd.handler) - if argspec.defaults is not None: - defaults = dict(zip(reversed(argspec.args), - reversed(list(argspec.defaults)))) - else: - defaults = {} - words.append(name) - minargs, maxargs = cmd.nargs - i = 1 - for arg in argspec.args: - if arg in ['self', 'count']: - continue - if minargs is not None and i <= minargs: - words.append('<{}>'.format(arg)) - elif maxargs is None or i <= maxargs: - words.append('[<{}>]'.format(arg)) - i += 1 - if argspec.varargs is not None: - words.append('[<{name}> [...]]'.format(name=argspec.varargs)) - return (' '.join(words), defaults) + usage = cmd.parser.format_usage() + if usage.startswith('usage: '): + usage = usage[7:] + return usage def _get_command_quickref(cmds): @@ -105,7 +88,7 @@ def _get_command_doc(name, cmd): """Generate the documentation for a command.""" output = ['[[cmd-{}]]'.format(name)] output += ['==== {}'.format(name)] - syntax, defaults = _get_cmd_syntax(name, cmd) + syntax = _get_cmd_syntax(name, cmd) if syntax != name: output.append('Syntax: +:{}+'.format(syntax)) output.append("") @@ -119,12 +102,6 @@ def _get_command_doc(name, cmd): text = desc.splitlines() firstline = text[0].replace(', or None', '') item = "* +{}+: {}".format(arg, firstline) - if arg in defaults: - val = defaults[arg] - if val is None: - item += " (optional)\n" - else: - item += " (default: +{}+)\n".format(defaults[arg]) item += '\n'.join(text[1:]) output.append(item) output.append("") From 47f42f9e5a796052bc83fb1a0369cc150d59dfa8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 6 Sep 2014 21:37:19 +0200 Subject: [PATCH 12/89] commands.argparser: Make type tuples a set(). --- qutebrowser/commands/argparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index 220c8be12..d323213fd 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -97,7 +97,7 @@ def multitype_conv(tpl): def _convert(value): """Convert a value according to an iterable of possible arg types.""" - for typ in tpl: + for typ in set(tpl): if isinstance(typ, str): if value == typ: return value From 13a2506c6a9dffff3bd0c065a91ba018167de274 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 6 Sep 2014 21:37:48 +0200 Subject: [PATCH 13/89] argparser: Also catch TypeError for multitype_conv. --- qutebrowser/commands/argparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index d323213fd..01e44e79e 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -109,7 +109,7 @@ def multitype_conv(tpl): return value try: return typ(value) - except ValueError: + except (TypeError, ValueError): pass raise cmdexc.ArgumentTypeError('Invalid value {}.'.format(value)) From 6674eedfae8edd18ec16b83334ddd3752eb7c9c5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 6 Sep 2014 21:38:05 +0200 Subject: [PATCH 14/89] cmdutils: Add default value type to multitype_conv tuple. --- qutebrowser/commands/cmdutils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 33cace1dc..2e3e8c344 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -238,6 +238,8 @@ class register: # pylint: disable=invalid-name if utils.is_enum(typ): type_conv[param.name] = argparser.enum_getter(typ) elif isinstance(typ, tuple): + if param.default is not inspect.Parameter.empty: + typ = typ + (type(param.default),) type_conv[param.name] = argparser.multitype_conv(typ) callsig = debugutils.format_call(self.parser.add_argument, args, kwargs, full=False) From a1fabcc5c2aca29631b8aa6f223da3e952a2c8db Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 5 Sep 2014 07:45:47 +0200 Subject: [PATCH 15/89] Start rewriting manpage generation --- .gitignore | 1 - doc/qutebrowser.1.asciidoc | 125 +++++++++++++++++++++++ scripts/generate_doc.py | 201 ++++++++++++------------------------- 3 files changed, 191 insertions(+), 136 deletions(-) create mode 100644 doc/qutebrowser.1.asciidoc diff --git a/.gitignore b/.gitignore index b0f6073df..6f8b588a7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,5 @@ __pycache__ /setuptools-*.zip /qutebrowser/git-commit-id # We can probably remove these later -*.asciidoc /doc/*.html /README.html diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc new file mode 100644 index 000000000..bd847fb64 --- /dev/null +++ b/doc/qutebrowser.1.asciidoc @@ -0,0 +1,125 @@ +// Note some sections in this file (everything between QUTE_*_START and +// QUTE_*_END) are autogenerated by scripts/generate_doc.sh. DO NOT edit them +// by hand. + += qutebrowser(1) +:doctype: manpage +:man source: qutebrowser +:man manual: qutebrowser manpage +:toc: +:homepage: http://www.qutebrowser.org/ + +== NAME +qutebrowser - A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit. + +== SYNOPSIS +*qutebrowser* ['-OPTION' ['...']] [':COMMAND' ['...']] ['URL' ['...']] + +== DESCRIPTION +qutebrowser is a keyboard-focused browser with with a minimal GUI. It's based +on Python, PyQt5 and QtWebKit and free software, licensed under the GPL. + +It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. + +== OPTIONS +// QUTE_OPTIONS_START +=== positional arguments +*':command'*:: + Commands to execute on startup. + +*'URL'*:: + URLs to open on startup. + + +=== optional arguments +*-h*, *--help*:: + show this help message and exit + +*-c* 'CONFDIR', *--confdir* 'CONFDIR':: + Set config directory (empty for no config storage) + +*-V*, *--version*:: + Show version and quit. + + +=== debug arguments +*-l* 'LOGLEVEL', *--loglevel* 'LOGLEVEL':: + Set loglevel + +*--logfilter* 'LOGFILTER':: + Comma-separated list of things to be logged to the debug log on stdout. + +*--loglines* 'LOGLINES':: + How many lines of the debug log to keep in RAM (-1: unlimited). + +*--debug*:: + Turn on debugging options. + +*--nocolor*:: + Turn off colored logging. + +*--harfbuzz* '{old,new,system,auto}':: + HarfBuzz engine version to use. Default: auto. + +*--nowindow*:: + Don't show the main window. + +*--debug-exit*:: + Turn on debugging of late exit. + +*--qt-style* 'STYLE':: + Set the Qt GUI style to use. + +*--qt-stylesheet* 'STYLESHEET':: + Override the Qt application stylesheet. + +*--qt-widgetcount*:: + Print debug message at the end about number of widgets left undestroyed and maximum number of widgets existed at the same time. + +*--qt-reverse*:: + Set the application's layout direction to right-to-left. + +*--qt-qmljsdebugger* 'port:PORT[,block]':: + Activate the QML/JS debugger with a specified port. 'block' is optional and will make the application wait until a debugger connects to it. + + + +// QUTE_OPTIONS_END + +== BUGS +Bugs are tracked at two locations: + +* The link:BUGS[doc/BUGS] and link:TODO[doc/TODO] files shipped with +qutebrowser. +* The Github issue tracker at +https://github.com/The-Compiler/qutebrowser/issues. + +If you found a bug or have a suggestion, either open a ticket in the github +issue tracker, or write a mail to the +https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at +mailto:qutebrowser@lists.qutebrowser.org[]. + +== COPYRIGHT +This program 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. + +This program 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 +this program. If not, see . + +== RESOURCES +* Website: http://www.qutebrowser.org/ +* Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] / +https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser +* IRC: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] +http://freenode.net/[Freenode] +* Github: https://github.com/The-Compiler/qutebrowser + +== AUTHOR +*qutebrowser* was written by Florian Bruhin. All contributors can be found in +the README file distributed with qutebrowser. diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index c430d4ff6..87bc6652f 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -154,66 +154,6 @@ def _format_action(action): return '{}\n {}\n\n'.format(invocation, action.help) -def generate_manpage_header(f): - """Generate an asciidoc header for the manpage.""" - f.write("// DO NOT EDIT THIS FILE BY HAND!\n") - f.write("// It is generated by `scripts/generate_doc.py`.\n") - f.write("// Most likely you'll need to rerun that script, or edit that " - "instead of this file.\n") - f.write('= qutebrowser(1)\n') - f.write(':doctype: manpage\n') - f.write(':man source: qutebrowser\n') - f.write(':man manual: qutebrowser manpage\n') - f.write(':toc:\n') - f.write(':homepage: http://www.qutebrowser.org/\n') - f.write('\n') - - -def generate_manpage_name(f): - """Generate the NAME-section of the manpage.""" - f.write('== NAME\n') - f.write('qutebrowser - {}\n'.format(qutebrowser.__description__)) - f.write('\n') - - -def generate_manpage_synopsis(f): - """Generate the SYNOPSIS-section of the manpage.""" - f.write('== SYNOPSIS\n') - f.write("*qutebrowser* ['-OPTION' ['...']] [':COMMAND' ['...']] " - "['URL' ['...']]\n") - f.write('\n') - - -def generate_manpage_description(f): - """Generate the DESCRIPTION-section of the manpage.""" - f.write('== DESCRIPTION\n') - f.write("qutebrowser is a keyboard-focused browser with with a minimal " - "GUI. It's based on Python, PyQt5 and QtWebKit and free software, " - "licensed under the GPL.\n\n") - f.write("It was inspired by other browsers/addons like dwb and " - "Vimperator/Pentadactyl.\n\n") - - -def generate_manpage_options(f): - """Generate the OPTIONS-section of the manpage from an argparse parser.""" - # pylint: disable=protected-access - parser = qutequtebrowser.get_argparser() - f.write('== OPTIONS\n') - - # positionals, optionals and user-defined groups - for group in parser._action_groups: - f.write('=== {}\n'.format(group.title)) - if group.description is not None: - f.write(group.description + '\n') - for action in group._group_actions: - f.write(_format_action(action)) - f.write('\n') - # epilog - if parser.epilog is not None: - f.write(parser.epilog) - f.write('\n') - - def generate_commands(f): """Generate the complete commands section.""" f.write('\n') @@ -297,89 +237,80 @@ def _get_authors(): return reversed(sorted(cnt, key=lambda k: cnt[k])) -def generate_manpage_author(f): - """Generate the manpage AUTHOR section.""" - f.write("== AUTHOR\n") - f.write("Contributors, sorted by the number of commits in descending " - "order:\n\n") - for author in _get_authors(): - f.write('* {}\n'.format(author)) - f.write('\n') +def _format_block(filename, what, data): + """Format a block in a file. + The block is delimited by markers like these: + // QUTE_*_START + ... + // QUTE_*_END -def generate_manpage_bugs(f): - """Generate the manpage BUGS section.""" - f.write('== BUGS\n') - f.write("Bugs are tracked at two locations:\n\n") - f.write("* The link:BUGS[doc/BUGS] and link:TODO[doc/TODO] files shipped " - "with qutebrowser.\n") - f.write("* The Github issue tracker at https://github.com/The-Compiler/" - "qutebrowser/issues .\n\n") - f.write("If you found a bug or have a suggestion, either open a ticket " - "in the github issue tracker, or write a mail to the " - "https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[" - "mailinglist] at mailto:qutebrowser@lists.qutebrowser.org[].\n\n") + The * part is the part which should be given as 'what'. - -def generate_manpage_copyright(f): - """Generate the COPYRIGHT section of the manpage.""" - f.write('== COPYRIGHT\n') - f.write("This program 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.\n\n") - f.write("This program 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.\n\n") - f.write("You should have received a copy of the GNU General Public " - "License along with this program. If not, see " - ".\n") - - -def generate_manpage_resources(f): - """Generate the RESOURCES section of the manpage.""" - f.write('== RESOURCES\n\n') - f.write("* Website: http://www.qutebrowser.org/\n") - f.write("* Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] / " - "https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser\n") - f.write("* IRC: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on " - "http://freenode.net/[Freenode]\n") - f.write("* Github: https://github.com/The-Compiler/qutebrowser\n\n") + Args: + filename: The file to change. + what: What to change (authors, options, etc.) + data; A list of strings which is the new data. + """ + what = what.upper() + oshandle, tmpname = tempfile.mkstemp() + try: + with _open_file(filename, mode='r') as infile, \ + _open_file(oshandle, mode='w') as temp: + found_start = False + found_end = False + for line in infile: + if line.strip() == '// QUTE_{}_START'.format(what): + temp.write(line) + temp.write(''.join(data)) + found_start = True + elif line.strip() == '// QUTE_{}_END'.format(what.upper()): + temp.write(line) + found_end = True + elif (not found_start) or found_end: + temp.write(line) + if not found_start: + raise Exception("Marker '// QUTE_{}_START' not found in " + "'{}'!".format(what, filename)) + elif not found_end: + raise Exception("Marker '// QUTE_{}_END' not found in " + "'{}'!".format(what, filename)) + except: # pylint: disable=bare-except + os.remove(tmpname) + raise + else: + os.remove(filename) + shutil.move(tmpname, filename) def regenerate_authors(filename): """Re-generate the authors inside README based on the commits made.""" - oshandle, tmpname = tempfile.mkstemp() - with _open_file(filename, mode='r') as infile, \ - _open_file(oshandle, mode='w') as temp: - ignore = False - for line in infile: - if line.strip() == '// QUTE_AUTHORS_START': - ignore = True - temp.write(line) - for author in _get_authors(): - temp.write('* {}\n'.format(author)) - elif line.strip() == '// QUTE_AUTHORS_END': - temp.write(line) - ignore = False - elif not ignore: - temp.write(line) - os.remove(filename) - shutil.move(tmpname, filename) + data = ['* {}\n'.format(author) for author in _get_authors()] + _format_block(filename, 'authors', data) + + +def regenerate_manpage(filename): + """Update manpage OPTIONS using an argparse parser.""" + # pylint: disable=protected-access + parser = qutequtebrowser.get_argparser() + options = [] + # positionals, optionals and user-defined groups + for group in parser._action_groups: + options.append('=== {}\n'.format(group.title)) + if group.description is not None: + options.append(group.description + '\n') + for action in group._group_actions: + options.append(_format_action(action)) + options.append('\n') + # epilog + if parser.epilog is not None: + options.append(parser.epilog) + options.append('\n') + _format_block(filename, 'options', options) if __name__ == '__main__': - with _open_file('doc/qutebrowser.1.asciidoc') as fobj: - generate_manpage_header(fobj) - generate_manpage_name(fobj) - generate_manpage_synopsis(fobj) - generate_manpage_description(fobj) - generate_manpage_options(fobj) - generate_settings(fobj) - generate_commands(fobj) - generate_manpage_bugs(fobj) - generate_manpage_author(fobj) - generate_manpage_resources(fobj) - generate_manpage_copyright(fobj) + regenerate_manpage('doc/qutebrowser.1.asciidoc') + #generate_settings(fobj) + #generate_commands(fobj) regenerate_authors('README.asciidoc') From 02292d8518c5406a32cf929b88f6d9355588eea5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 19:04:09 +0200 Subject: [PATCH 16/89] generate_doc: Remove unneeded whitespace --- doc/qutebrowser.1.asciidoc | 5 ----- scripts/generate_doc.py | 15 ++++++++------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index bd847fb64..7761b790d 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -30,7 +30,6 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. *'URL'*:: URLs to open on startup. - === optional arguments *-h*, *--help*:: show this help message and exit @@ -41,7 +40,6 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. *-V*, *--version*:: Show version and quit. - === debug arguments *-l* 'LOGLEVEL', *--loglevel* 'LOGLEVEL':: Set loglevel @@ -81,9 +79,6 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. *--qt-qmljsdebugger* 'port:PORT[,block]':: Activate the QML/JS debugger with a specified port. 'block' is optional and will make the application wait until a debugger connects to it. - - - // QUTE_OPTIONS_END == BUGS diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 87bc6652f..659b3aaa3 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -151,7 +151,7 @@ def _format_action(action): for opt in action.option_strings: parts.append('*{}* {}'.format(opt, args_string)) invocation = ', '.join(parts) + '::' - return '{}\n {}\n\n'.format(invocation, action.help) + return '{}\n {}\n'.format(invocation, action.help) def generate_commands(f): @@ -293,19 +293,20 @@ def regenerate_manpage(filename): """Update manpage OPTIONS using an argparse parser.""" # pylint: disable=protected-access parser = qutequtebrowser.get_argparser() - options = [] + groups = [] # positionals, optionals and user-defined groups for group in parser._action_groups: - options.append('=== {}\n'.format(group.title)) + groupdata = [] + groupdata.append('=== {}'.format(group.title)) if group.description is not None: - options.append(group.description + '\n') + groupdata.append(group.description) for action in group._group_actions: - options.append(_format_action(action)) - options.append('\n') + groupdata.append(_format_action(action)) + groups.append('\n'.join(groupdata)) + options = '\n'.join(groups) # epilog if parser.epilog is not None: options.append(parser.epilog) - options.append('\n') _format_block(filename, 'options', options) From 9f23e9aa369a90ad805b718926d103d05824e4ad Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 20:11:38 +0200 Subject: [PATCH 17/89] Many improvements for generate_doc --- scripts/generate_doc.py | 224 +++++++++++++++++++++++++--------------- 1 file changed, 142 insertions(+), 82 deletions(-) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 659b3aaa3..4d89e2d39 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -28,6 +28,7 @@ import inspect import subprocess import collections import tempfile +import argparse sys.path.insert(0, os.getcwd()) @@ -40,16 +41,71 @@ from qutebrowser.config import configdata from qutebrowser.utils import utils +class UsageFormatter(argparse.HelpFormatter): + + """Patched HelpFormatter to include some asciidoc markup in the usage. + + This does some horrible things, but the alternative would be to reimplement + argparse.HelpFormatter while copying 99% of the code :-/ + """ + + def _format_usage(self, usage, actions, groups, _prefix): + """Override _format_usage to not add the 'usage:' prefix.""" + return super()._format_usage(usage, actions, groups, '') + + def _metavar_formatter(self, action, default_metavar): + """Override _metavar_formatter to add asciidoc markup to metavars. + + Most code here is copied from Python 3.4's argparse.py. + """ + if action.metavar is not None: + result = "'{}'".format(action.metavar) + elif action.choices is not None: + choice_strs = [str(choice) for choice in action.choices] + result = '{%s}' % ','.join('*{}*'.format(e) for e in choice_strs) + else: + result = "'{}'".format(default_metavar) + + def fmt(tuple_size): + if isinstance(result, tuple): + return result + else: + return (result, ) * tuple_size + return fmt + + def _format_actions_usage(self, actions, groups): + """Override _format_actions_usage to add asciidoc markup to flags. + + Because argparse.py's _format_actions_usage is very complex, we first + monkey-patch the option strings to include the asciidoc markup, then + run the original method, then undo the patching. + """ + old_option_strings = {} + for action in actions: + old_option_strings[action] = action.option_strings[:] + action.option_strings = ['*{}*'.format(s) + for s in action.option_strings] + ret = super()._format_actions_usage(actions, groups) + for action in actions: + action.option_strings = old_option_strings[action] + return ret + + def _open_file(name, mode='w'): """Open a file with a preset newline/encoding mode.""" return open(name, mode, newline='\n', encoding='utf-8') def _get_cmd_syntax(name, cmd): - """Get the command syntax for a command.""" - usage = cmd.parser.format_usage() - if usage.startswith('usage: '): - usage = usage[7:] + """Get the command syntax for a command. + + We monkey-patch the parser's formatter_class here to use our UsageFormatter + which adds some asciidoc markup. + """ + old_fmt_class = cmd.parser.formatter_class + cmd.parser.formatter_class = UsageFormatter + usage = cmd.parser.format_usage().rstrip() + cmd.parser.formatter_class = old_fmt_class return usage @@ -61,7 +117,7 @@ def _get_command_quickref(cmds): out.append('|Command|Description') for name, cmd in cmds: desc = inspect.getdoc(cmd.handler).splitlines()[0] - out.append('|<>|{}'.format(name, name, desc)) + out.append('|<<{},{}>>|{}'.format(name, name, desc)) out.append('|==============') return '\n'.join(out) @@ -86,16 +142,17 @@ def _get_setting_quickref(): def _get_command_doc(name, cmd): """Generate the documentation for a command.""" - output = ['[[cmd-{}]]'.format(name)] + output = ['[[{}]]'.format(name)] output += ['==== {}'.format(name)] syntax = _get_cmd_syntax(name, cmd) if syntax != name: output.append('Syntax: +:{}+'.format(syntax)) - output.append("") + output.append("") parser = utils.DocstringParser(cmd.handler) output.append(parser.short_desc) - output.append("") - output.append(parser.long_desc) + if parser.long_desc: + output.append("") + output.append(parser.long_desc) if parser.arg_descs: output.append("") for arg, desc in parser.arg_descs.items(): @@ -104,7 +161,7 @@ def _get_command_doc(name, cmd): item = "* +{}+: {}".format(arg, firstline) item += '\n'.join(text[1:]) output.append(item) - output.append("") + output.append("") output.append("") return '\n'.join(output) @@ -154,80 +211,83 @@ def _format_action(action): return '{}\n {}\n'.format(invocation, action.help) -def generate_commands(f): +def generate_commands(filename): """Generate the complete commands section.""" - f.write('\n') - f.write("== COMMANDS\n") - normal_cmds = [] - hidden_cmds = [] - debug_cmds = [] - for name, cmd in cmdutils.cmd_dict.items(): - if cmd.hide: - hidden_cmds.append((name, cmd)) - elif cmd.debug: - debug_cmds.append((name, cmd)) - else: - normal_cmds.append((name, cmd)) - normal_cmds.sort() - hidden_cmds.sort() - debug_cmds.sort() - f.write("\n") - f.write("=== Normal commands\n") - f.write(".Quick reference\n") - f.write(_get_command_quickref(normal_cmds) + "\n") - for name, cmd in normal_cmds: - f.write(_get_command_doc(name, cmd) + "\n") - f.write("\n") - f.write("=== Hidden commands\n") - f.write(".Quick reference\n") - f.write(_get_command_quickref(hidden_cmds) + "\n") - for name, cmd in hidden_cmds: - f.write(_get_command_doc(name, cmd) + "\n") - f.write("\n") - f.write("=== Debugging commands\n") - f.write("These commands are mainly intended for debugging. They are " - "hidden if qutebrowser was started without the `--debug`-flag.\n") - f.write("\n") - f.write(".Quick reference\n") - f.write(_get_command_quickref(debug_cmds) + "\n") - for name, cmd in debug_cmds: - f.write(_get_command_doc(name, cmd) + "\n") - - -def generate_settings(f): - """Generate the complete settings section.""" - f.write("\n") - f.write("== SETTINGS\n") - f.write(_get_setting_quickref() + "\n") - for sectname, sect in configdata.DATA.items(): + with _open_file(filename) as f: + f.write('\n') + f.write("== COMMANDS\n") + normal_cmds = [] + hidden_cmds = [] + debug_cmds = [] + for name, cmd in cmdutils.cmd_dict.items(): + if cmd.hide: + hidden_cmds.append((name, cmd)) + elif cmd.debug: + debug_cmds.append((name, cmd)) + else: + normal_cmds.append((name, cmd)) + normal_cmds.sort() + hidden_cmds.sort() + debug_cmds.sort() f.write("\n") - f.write("=== {}".format(sectname) + "\n") - f.write(configdata.SECTION_DESC[sectname] + "\n") - if not getattr(sect, 'descriptions'): - pass - else: - for optname, option in sect.items(): - f.write("\n") - f.write('[[setting-{}-{}]]'.format(sectname, optname) + "\n") - f.write("==== {}".format(optname) + "\n") - f.write(sect.descriptions[optname] + "\n") - f.write("\n") - valid_values = option.typ.valid_values - if valid_values is not None: - f.write("Valid values:\n") + f.write("=== Normal commands\n") + f.write(".Quick reference\n") + f.write(_get_command_quickref(normal_cmds) + "\n\n") + for name, cmd in normal_cmds: + f.write(_get_command_doc(name, cmd)) + f.write("\n") + f.write("=== Hidden commands\n") + f.write(".Quick reference\n") + f.write(_get_command_quickref(hidden_cmds)) + for name, cmd in hidden_cmds: + f.write(_get_command_doc(name, cmd)) + f.write("\n") + f.write("=== Debugging commands\n") + f.write("These commands are mainly intended for debugging. They are " + "hidden if qutebrowser was started without the " + "`--debug`-flag.\n") + f.write("\n") + f.write(".Quick reference\n") + f.write(_get_command_quickref(debug_cmds)) + for name, cmd in debug_cmds: + f.write(_get_command_doc(name, cmd)) + + +def generate_settings(filename): + """Generate the complete settings section.""" + with _open_file(filename) as f: + f.write("\n") + f.write("== SETTINGS\n") + f.write(_get_setting_quickref() + "\n") + for sectname, sect in configdata.DATA.items(): + f.write("\n") + f.write("=== {}".format(sectname) + "\n") + f.write(configdata.SECTION_DESC[sectname] + "\n") + if not getattr(sect, 'descriptions'): + pass + else: + for optname, option in sect.items(): f.write("\n") - for val in valid_values: - try: - desc = valid_values.descriptions[val] - f.write(" * +{}+: {}".format(val, desc) + "\n") - except KeyError: - f.write(" * +{}+".format(val) + "\n") + f.write('[[{}-{}]]'.format(sectname, optname) + "\n") + f.write("==== {}".format(optname) + "\n") + f.write(sect.descriptions[optname] + "\n") f.write("\n") - if option.default(): - f.write("Default: +pass:[{}]+\n".format(html.escape( - option.default()))) - else: - f.write("Default: empty\n") + valid_values = option.typ.valid_values + if valid_values is not None: + f.write("Valid values:\n") + f.write("\n") + for val in valid_values: + try: + desc = valid_values.descriptions[val] + f.write(" * +{}+: {}".format(val, desc) + "\n") + except KeyError: + f.write(" * +{}+".format(val) + "\n") + f.write("\n") + if option.default(): + f.write("Default: +pass:[{}]+\n".format(html.escape( + option.default()))) + else: + f.write("Default: empty\n") def _get_authors(): @@ -312,6 +372,6 @@ def regenerate_manpage(filename): if __name__ == '__main__': regenerate_manpage('doc/qutebrowser.1.asciidoc') - #generate_settings(fobj) - #generate_commands(fobj) + generate_settings('doc/settings.asciidoc') + generate_commands('doc/commands.asciidoc') regenerate_authors('README.asciidoc') From 4cf7e6e7672ffb1dc41ab02cab23dc07915b82f6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 21:03:20 +0200 Subject: [PATCH 18/89] Add docstring for :quickmark-load. --- qutebrowser/browser/commands.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0a54db600..f4e730670 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -620,7 +620,13 @@ class CommandDispatcher: @cmdutils.register(instance='mainwindow.tabs.cmd') def quickmark_load(self, name, tab=False, bg=False): - """Load a quickmark.""" + """Load a quickmark. + + Args: + name: The name of the quickmark to load. + tab: Whether to load the quickmark in a new tab. + bg: Whether to load the quickmark in the background. + """ urlstr = quickmarks.get(name) url = QUrl(urlstr) if not url.isValid(): From dcfb52847fa20472cd1660530115242c7b2e52b1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 21:04:39 +0200 Subject: [PATCH 19/89] Nicer flag output in docs. --- qutebrowser/commands/cmdutils.py | 13 ++++++++++--- qutebrowser/commands/command.py | 4 +++- scripts/generate_doc.py | 28 +++++++++++++++++++++------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 2e3e8c344..1188cf1f2 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -138,6 +138,8 @@ class register: # pylint: disable=invalid-name self.parser = None self.func = None self.docparser = None + self.opt_args = collections.OrderedDict() + self.pos_args = [] if modes is not None: for m in modes: if not isinstance(m, usertypes.KeyMode): @@ -180,7 +182,8 @@ class register: # pylint: disable=invalid-name desc=desc, instance=self.instance, handler=func, completion=self.completion, modes=self.modes, not_modes=self.not_modes, needs_js=self.needs_js, - is_debug=self.debug, parser=self.parser, type_conv=type_conv) + is_debug=self.debug, parser=self.parser, type_conv=type_conv, + opt_args=self.opt_args, pos_args=self.pos_args) for name in names: cmd_dict[name] = cmd return func @@ -259,10 +262,14 @@ class register: # pylint: disable=invalid-name name = annotation_info.name or param.name shortname = annotation_info.flag or param.name[0] if self._get_type(param, annotation_info) == bool: - args.append('--{}'.format(name)) - args.append('-{}'.format(shortname)) + long_flag = '--{}'.format(name) + short_flag = '-{}'.format(shortname) + args.append(long_flag) + args.append(short_flag) + self.opt_args[name] = long_flag, short_flag else: args.append(name) + self.pos_args.append(name) return args def _param_to_argparse_kw(self, param, annotation_info): diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 58b3b9911..996b879a4 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -52,7 +52,7 @@ class Command: def __init__(self, name, split, hide, count, desc, instance, handler, completion, modes, not_modes, needs_js, is_debug, parser, - type_conv): + type_conv, opt_args, pos_args): # I really don't know how to solve this in a better way, I tried. # pylint: disable=too-many-arguments self.name = name @@ -69,6 +69,8 @@ class Command: self.debug = is_debug self.parser = parser self.type_conv = type_conv + self.opt_args = opt_args + self.pos_args = pos_args def _check_prerequisites(self): """Check if the command is permitted to run currently. diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 4d89e2d39..134134f24 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -153,14 +153,28 @@ def _get_command_doc(name, cmd): if parser.long_desc: output.append("") output.append(parser.long_desc) - if parser.arg_descs: + + if cmd.pos_args: output.append("") - for arg, desc in parser.arg_descs.items(): - text = desc.splitlines() - firstline = text[0].replace(', or None', '') - item = "* +{}+: {}".format(arg, firstline) - item += '\n'.join(text[1:]) - output.append(item) + output.append('===== {}'.format("positional arguments")) + for arg in cmd.pos_args: + try: + output.append('* +{}+: {}'.format(arg, parser.arg_descs[arg])) + except KeyError as e: + raise KeyError("No description for arg {} of command " + "'{}'!".format(e, cmd.name)) + + if cmd.opt_args: + output.append("") + output.append('===== {}'.format("optional arguments")) + for arg, (long_flag, short_flag) in cmd.opt_args.items(): + try: + output.append('* +{}+, +{}+: {}'.format( + short_flag, long_flag, parser.arg_descs[arg])) + except KeyError: + raise KeyError("No description for arg {} of command " + "'{}'!".format(e, cmd.name)) + output.append("") output.append("") return '\n'.join(output) From fc70d700b2bb05d0209bc5234a105fef75461a10 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 21:06:36 +0200 Subject: [PATCH 20/89] cmdutils: Force metavar if choices are given. --- qutebrowser/commands/cmdutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 1188cf1f2..ca15f59e8 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -291,6 +291,7 @@ class register: # pylint: disable=invalid-name pass elif utils.is_enum(typ): kwargs['choices'] = [e.name.replace('_', '-') for e in typ] + kwargs['metavar'] = param.name elif typ is bool: kwargs['action'] = 'store_true' elif typ is not None: From 9e3f5e28bc3a636addd0ff956eea8c3820563ad5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 21:09:21 +0200 Subject: [PATCH 21/89] docs: Fix heading levels --- scripts/generate_doc.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 134134f24..fc72b5a79 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -143,7 +143,7 @@ def _get_setting_quickref(): def _get_command_doc(name, cmd): """Generate the documentation for a command.""" output = ['[[{}]]'.format(name)] - output += ['==== {}'.format(name)] + output += ['=== {}'.format(name)] syntax = _get_cmd_syntax(name, cmd) if syntax != name: output.append('Syntax: +:{}+'.format(syntax)) @@ -156,7 +156,7 @@ def _get_command_doc(name, cmd): if cmd.pos_args: output.append("") - output.append('===== {}'.format("positional arguments")) + output.append("==== positional arguments") for arg in cmd.pos_args: try: output.append('* +{}+: {}'.format(arg, parser.arg_descs[arg])) @@ -166,7 +166,7 @@ def _get_command_doc(name, cmd): if cmd.opt_args: output.append("") - output.append('===== {}'.format("optional arguments")) + output.append("==== optional arguments") for arg, (long_flag, short_flag) in cmd.opt_args.items(): try: output.append('* +{}+, +{}+: {}'.format( @@ -229,7 +229,7 @@ def generate_commands(filename): """Generate the complete commands section.""" with _open_file(filename) as f: f.write('\n') - f.write("== COMMANDS\n") + f.write("= Commands\n") normal_cmds = [] hidden_cmds = [] debug_cmds = [] @@ -244,19 +244,19 @@ def generate_commands(filename): hidden_cmds.sort() debug_cmds.sort() f.write("\n") - f.write("=== Normal commands\n") + f.write("== Normal commands\n") f.write(".Quick reference\n") f.write(_get_command_quickref(normal_cmds) + "\n\n") for name, cmd in normal_cmds: f.write(_get_command_doc(name, cmd)) f.write("\n") - f.write("=== Hidden commands\n") + f.write("== Hidden commands\n") f.write(".Quick reference\n") f.write(_get_command_quickref(hidden_cmds)) for name, cmd in hidden_cmds: f.write(_get_command_doc(name, cmd)) f.write("\n") - f.write("=== Debugging commands\n") + f.write("== Debugging commands\n") f.write("These commands are mainly intended for debugging. They are " "hidden if qutebrowser was started without the " "`--debug`-flag.\n") @@ -271,11 +271,11 @@ def generate_settings(filename): """Generate the complete settings section.""" with _open_file(filename) as f: f.write("\n") - f.write("== SETTINGS\n") + f.write("= Settings\n") f.write(_get_setting_quickref() + "\n") for sectname, sect in configdata.DATA.items(): f.write("\n") - f.write("=== {}".format(sectname) + "\n") + f.write("== {}".format(sectname) + "\n") f.write(configdata.SECTION_DESC[sectname] + "\n") if not getattr(sect, 'descriptions'): pass @@ -283,7 +283,7 @@ def generate_settings(filename): for optname, option in sect.items(): f.write("\n") f.write('[[{}-{}]]'.format(sectname, optname) + "\n") - f.write("==== {}".format(optname) + "\n") + f.write("=== {}".format(optname) + "\n") f.write(sect.descriptions[optname] + "\n") f.write("\n") valid_values = option.typ.valid_values From 04c77d4d90d454974d161e0d9c0823cc9306a954 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 21:17:19 +0200 Subject: [PATCH 22/89] Adjust formatting for arg descriptions --- scripts/generate_doc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index fc72b5a79..5dd17f399 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -159,7 +159,8 @@ def _get_command_doc(name, cmd): output.append("==== positional arguments") for arg in cmd.pos_args: try: - output.append('* +{}+: {}'.format(arg, parser.arg_descs[arg])) + output.append("* +'{}'+: {}".format(arg, + parser.arg_descs[arg])) except KeyError as e: raise KeyError("No description for arg {} of command " "'{}'!".format(e, cmd.name)) @@ -169,7 +170,7 @@ def _get_command_doc(name, cmd): output.append("==== optional arguments") for arg, (long_flag, short_flag) in cmd.opt_args.items(): try: - output.append('* +{}+, +{}+: {}'.format( + output.append('* +*{}*+, +*{}*+: {}'.format( short_flag, long_flag, parser.arg_descs[arg])) except KeyError: raise KeyError("No description for arg {} of command " From e416228713e0a64626edce35904afecc87a28082 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 21:19:04 +0200 Subject: [PATCH 23/89] doc: Add missing blank lines --- scripts/generate_doc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 5dd17f399..2f8992422 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -247,13 +247,13 @@ def generate_commands(filename): f.write("\n") f.write("== Normal commands\n") f.write(".Quick reference\n") - f.write(_get_command_quickref(normal_cmds) + "\n\n") + f.write(_get_command_quickref(normal_cmds) + '\n') for name, cmd in normal_cmds: f.write(_get_command_doc(name, cmd)) f.write("\n") f.write("== Hidden commands\n") f.write(".Quick reference\n") - f.write(_get_command_quickref(hidden_cmds)) + f.write(_get_command_quickref(hidden_cmds) + '\n') for name, cmd in hidden_cmds: f.write(_get_command_doc(name, cmd)) f.write("\n") @@ -263,7 +263,7 @@ def generate_commands(filename): "`--debug`-flag.\n") f.write("\n") f.write(".Quick reference\n") - f.write(_get_command_quickref(debug_cmds)) + f.write(_get_command_quickref(debug_cmds) + '\n') for name, cmd in debug_cmds: f.write(_get_command_doc(name, cmd)) From 8d9dd8e83d5fe1d1f094f54d54b10ad9eac2b476 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 22:43:38 +0200 Subject: [PATCH 24/89] doc: Remove some newlines --- scripts/generate_doc.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 2f8992422..77b0a0242 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -229,7 +229,6 @@ def _format_action(action): def generate_commands(filename): """Generate the complete commands section.""" with _open_file(filename) as f: - f.write('\n') f.write("= Commands\n") normal_cmds = [] hidden_cmds = [] @@ -271,7 +270,6 @@ def generate_commands(filename): def generate_settings(filename): """Generate the complete settings section.""" with _open_file(filename) as f: - f.write("\n") f.write("= Settings\n") f.write(_get_setting_quickref() + "\n") for sectname, sect in configdata.DATA.items(): From 02e4fdd8676b77696263c6436d227cab6d5aaaa8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 22:57:05 +0200 Subject: [PATCH 25/89] generate_doc: Call asciidoc --- scripts/generate_doc.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 77b0a0242..cfb0b4563 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -24,12 +24,15 @@ import os import sys import html import shutil +import os.path import inspect import subprocess import collections import tempfile import argparse +import colorama as col + sys.path.insert(0, os.getcwd()) import qutebrowser @@ -383,8 +386,32 @@ def regenerate_manpage(filename): _format_block(filename, 'options', options) +def call_asciidoc(src, dst): + print("{}Calling asciidoc for {}...{}".format( + col.Fore.CYAN, os.path.basename(src), col.Fore.RESET)) + args = ['asciidoc'] + if dst is not None: + args += ['--out-file', dst] + args.append(src) + try: + subprocess.check_call(args) + except subprocess.CalledProcessError as e: + print(''.join([col.Fore.RED, str(e), col.Fore.RESET])) + sys.exit(1) + + if __name__ == '__main__': + print("{}Generating asciidoc files...{}".format( + col.Fore.CYAN, col.Fore.RESET)) regenerate_manpage('doc/qutebrowser.1.asciidoc') generate_settings('doc/settings.asciidoc') generate_commands('doc/commands.asciidoc') regenerate_authors('README.asciidoc') + asciidoc_files = [('doc/qutebrowser.1.asciidoc', None), + ('doc/settings.asciidoc', + 'qutebrowser/doc/settings.html'), + ('doc/commands.asciidoc', + 'qutebrowser/doc/commands.html'), + ('README.asciidoc', None)] + for src, dst in asciidoc_files: + call_asciidoc(src, dst) From 2cc41081b3e6e97338a737a01be6a820376da0b0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 23:11:22 +0200 Subject: [PATCH 26/89] Add newline in settings doc. --- scripts/generate_doc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index cfb0b4563..3ee51bf2a 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -131,6 +131,7 @@ def _get_setting_quickref(): for sectname, sect in configdata.DATA.items(): if not getattr(sect, 'descriptions'): continue + out.append("") out.append(".Quick reference for section ``{}''".format(sectname)) out.append('[options="header",width="75%",cols="25%,75%"]') out.append('|==============') From 0267dac2be2ce39cdf189bfa2d793241985a1f61 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 7 Sep 2014 23:22:38 +0200 Subject: [PATCH 27/89] Add --help documentation --- doc/commands.asciidoc | 622 ++++++++++++++ doc/settings.asciidoc | 1255 +++++++++++++++++++++++++++++ qutebrowser/commands/argparser.py | 2 +- qutebrowser/network/qutescheme.py | 38 +- scripts/generate_doc.py | 4 +- 5 files changed, 1905 insertions(+), 16 deletions(-) create mode 100644 doc/commands.asciidoc create mode 100644 doc/settings.asciidoc diff --git a/doc/commands.asciidoc b/doc/commands.asciidoc new file mode 100644 index 000000000..edbe9b05d --- /dev/null +++ b/doc/commands.asciidoc @@ -0,0 +1,622 @@ += Commands + +== Normal commands +.Quick reference +[options="header",width="75%",cols="25%,75%"] +|============== +|Command|Description +|<>|Go back in the history of the current tab. +|<>|Cancel the first/[count]th download. +|<>|Download the current page. +|<>|Go forward in the history of the current tab. +|<>|Get the value from a section/option. +|<>|Start hinting. +|<>|Open main startpage in current tab. +|<>|Toggle the web inspector. +|<>|Execute a command after some time. +|<>|Open a "next" link. +|<>|Open a URL in the current/[count]th tab. +|<>|Open a page from the clipboard. +|<>|Open a "previous" link. +|<>|Print the current/[count]th tab. +|<>|Quit qutebrowser. +|<>|Add a new quickmark. +|<>|Load a quickmark. +|<>|Save the current page as a quickmark. +|<>|Quit qutebrowser. +|<>|Reload the current/[count]th tab. +|<>|Report a bug in qutebrowser. +|<>|Restart qutebrowser while keeping existing tabs open. +|<>|Run an userscript given as argument. +|<>|Save the config file. +|<>|Set an option. +|<>|Preset the statusbar to some text. +|<>|Spawn a command in a shell. +|<>|Stop loading in the current/[count]th tab. +|<>|Close the current/[count]th tab. +|<>|Select the tab given as argument/[count]. +|<>|Move the current tab. +|<>|Switch to the next tab, or switch [count] tabs forward. +|<>|Close all tabs except for the current one. +|<>|Switch to the previous tab, or switch [count] tabs back. +|<>|Re-open a closed tab (optionally skipping [count] closed tabs). +|<>|Yank the current URL/title to the clipboard or primary selection. +|<>|Set the zoom level for the current tab. +|<>|Increase the zoom level for the current tab. +|<>|Decrease the zoom level for the current tab. +|============== +[[back]] +=== back +Go back in the history of the current tab. + +[[cancel-download]] +=== cancel-download +Cancel the first/[count]th download. + +[[download-page]] +=== download-page +Download the current page. + +[[forward]] +=== forward +Go forward in the history of the current tab. + +[[get]] +=== get +Syntax: +:get 'sectname' 'optname'+ + +Get the value from a section/option. + +==== positional arguments +* +'sectname'+: The section where the option is in. +* +'optname'+: The name of the option. + +[[hint]] +=== hint +Syntax: +:hint ['group'] ['target'] ['args']+ + +Start hinting. + +==== positional arguments +* +'group'+: The hinting mode to use. + + - `all`: All clickable elements. + - `links`: Only links. + - `images`: Only images. + + + +* +'target'+: What to do with the selected element. + + - `normal`: Open the link in the current tab. + - `tab`: Open the link in a new tab. + - `tab-bg`: Open the link in a new background tab. + - `yank`: Yank the link to the clipboard. + - `yank-primary`: Yank the link to the primary selection. + - `fill`: Fill the commandline with the command given as + argument. + - `cmd-tab`: Fill the commandline with `:open -t` and the + link. + - `cmd-tag-bg`: Fill the commandline with `:open -b` and + the link. + - `rapid`: Open the link in a new tab and stay in hinting mode. + - `download`: Download the link. + - `userscript`: Call an userscript with `$QUTE_URL` set to the + link. + - `spawn`: Spawn a command. + + + +* +'args'+: Arguments for spawn/userscript/fill. + + - With `spawn`: The executable and arguments to spawn. + `{hint-url}` will get replaced by the selected + URL. + - With `userscript`: The userscript to execute. + - With `fill`: The command to fill the statusbar with. + `{hint-url}` will get replaced by the selected + URL. + + +[[home]] +=== home +Open main startpage in current tab. + +[[inspector]] +=== inspector +Toggle the web inspector. + +[[later]] +=== later +Syntax: +:later 'ms' 'command' ['command' ...]+ + +Execute a command after some time. + +==== positional arguments +* +'ms'+: How many milliseconds to wait. +* +'command'+: The command/args to run. + +[[next-page]] +=== next-page +Syntax: +:next-page [*--tab*]+ + +Open a "next" link. + +This tries to automatically click on typical "Next Page" links using some heuristics. + +==== optional arguments +* +*-t*+, +*--tab*+: Whether to open a new tab. + +[[open]] +=== open +Syntax: +:open [*--bg*] [*--tab*] 'urlstr'+ + +Open a URL in the current/[count]th tab. + +==== positional arguments +* +'urlstr'+: The URL to open, as string. + +==== optional arguments +* +*-b*+, +*--bg*+: Whether to open in a background tab. +* +*-t*+, +*--tab*+: Whether to open in a tab. + +[[paste]] +=== paste +Syntax: +:paste [*--sel*] [*--tab*]+ + +Open a page from the clipboard. + +==== optional arguments +* +*-s*+, +*--sel*+: True to use primary selection, False to use clipboard +* +*-t*+, +*--tab*+: True to open in a new tab. + +[[prev-page]] +=== prev-page +Syntax: +:prev-page [*--tab*]+ + +Open a "previous" link. + +This tries to automaticall click on typical "Previous Page" links using some heuristics. + +==== optional arguments +* +*-t*+, +*--tab*+: Whether to open a new tab. + +[[print]] +=== print +Syntax: +:print [*--preview*]+ + +Print the current/[count]th tab. + +==== optional arguments +* +*-p*+, +*--preview*+: Whether to preview instead of printing. + +[[q]] +=== q +Syntax: +:quit+ + +Quit qutebrowser. + +[[quickmark-add]] +=== quickmark-add +Syntax: +:quickmark-add 'urlstr' 'name'+ + +Add a new quickmark. + +==== positional arguments +* +'urlstr'+: The url to add as quickmark, as string. +* +'name'+: The name for the new quickmark. + +[[quickmark-load]] +=== quickmark-load +Syntax: +:quickmark-load [*--tab*] [*--bg*] 'name'+ + +Load a quickmark. + +==== positional arguments +* +'name'+: The name of the quickmark to load. + +==== optional arguments +* +*-t*+, +*--tab*+: Whether to load the quickmark in a new tab. +* +*-b*+, +*--bg*+: Whether to load the quickmark in the background. + +[[quickmark-save]] +=== quickmark-save +Save the current page as a quickmark. + +[[quit]] +=== quit +Quit qutebrowser. + +[[reload]] +=== reload +Reload the current/[count]th tab. + +[[report]] +=== report +Report a bug in qutebrowser. + +[[restart]] +=== restart +Restart qutebrowser while keeping existing tabs open. + +[[run-userscript]] +=== run-userscript +Syntax: +:run-userscript 'cmd' ['args' ['args' ...]]+ + +Run an userscript given as argument. + +==== positional arguments +* +'cmd'+: The userscript to run. +* +'args'+: Arguments to pass to the userscript. + +[[save]] +=== save +Save the config file. + +[[set]] +=== set +Syntax: +:set [*--temp*] 'sectname' 'optname' 'value'+ + +Set an option. + +==== positional arguments +* +'sectname'+: The section where the option is in. +* +'optname'+: The name of the option. +* +'value'+: The value to set. + +==== optional arguments +* +*-t*+, +*--temp*+: Set value temporarely. + +[[set-cmd-text]] +=== set-cmd-text +Syntax: +:set-cmd-text 'text'+ + +Preset the statusbar to some text. + +==== positional arguments +* +'text'+: The commandline to set. + +[[spawn]] +=== spawn +Syntax: +:spawn ['args' ['args' ...]]+ + +Spawn a command in a shell. + +Note the {url} variable which gets replaced by the current URL might be useful here. + +==== positional arguments +* +'args'+: The commandline to execute. + +[[stop]] +=== stop +Stop loading in the current/[count]th tab. + +[[tab-close]] +=== tab-close +Close the current/[count]th tab. + +[[tab-focus]] +=== tab-focus +Syntax: +:tab-focus ['index']+ + +Select the tab given as argument/[count]. + +==== positional arguments +* +'index'+: The tab index to focus, starting with 1. The special value `last` focuses the last focused tab. + + +[[tab-move]] +=== tab-move +Syntax: +:tab-move ['direction']+ + +Move the current tab. + +==== positional arguments +* +'direction'+: + or - for relative moving, none for absolute. + +[[tab-next]] +=== tab-next +Switch to the next tab, or switch [count] tabs forward. + +[[tab-only]] +=== tab-only +Close all tabs except for the current one. + +[[tab-prev]] +=== tab-prev +Switch to the previous tab, or switch [count] tabs back. + +[[undo]] +=== undo +Re-open a closed tab (optionally skipping [count] closed tabs). + +[[yank]] +=== yank +Syntax: +:yank [*--title*] [*--sel*]+ + +Yank the current URL/title to the clipboard or primary selection. + +==== optional arguments +* +*-t*+, +*--title*+: Whether to yank the title instead of the URL. +* +*-s*+, +*--sel*+: True to use primary selection, False to use clipboard + +[[zoom]] +=== zoom +Syntax: +:zoom ['zoom']+ + +Set the zoom level for the current tab. + +The zoom can be given as argument or as [count]. If neither of both is given, the zoom is set to 100%. + +==== positional arguments +* +'zoom'+: The zoom percentage to set. + +[[zoom-in]] +=== zoom-in +Increase the zoom level for the current tab. + +[[zoom-out]] +=== zoom-out +Decrease the zoom level for the current tab. + + +== Hidden commands +.Quick reference +[options="header",width="75%",cols="25%,75%"] +|============== +|Command|Description +|<>|Execute the command currently in the commandline. +|<>|Go forward in the commandline history. +|<>|Go back in the commandline history. +|<>|Select the next completion item. +|<>|Select the previous completion item. +|<>|Enter a key mode. +|<>|Follow the currently selected hint. +|<>|Leave the mode we're currently in. +|<>|Open an external editor with the currently selected form field. +|<>|Accept the current prompt. +|<>|Answer no to a yes/no prompt. +|<>|Answer yes to a yes/no prompt. +|<>|Move back a character. +|<>|Delete the character before the cursor. +|<>|Move back to the start of the current or previous word. +|<>|Move to the start of the line. +|<>|Delete the character after the cursor. +|<>|Move to the end of the line. +|<>|Move forward a character. +|<>|Move forward to the end of the next word. +|<>|Remove chars from the cursor to the end of the line. +|<>|Remove chars from the cursor to the end of the current word. +|<>|Remove chars backward from the cursor to the beginning of the line. +|<>|Remove chars from the cursor to the beginning of the word. +|<>|Paste the most recently deleted text. +|<>|Scroll the current tab by 'count * dx/dy'. +|<>|Scroll the frame page-wise. +|<>|Scroll to a specific percentage of the page. +|<>|Continue the search to the ([count]th) next term. +|<>|Continue the search to the ([count]th) previous term. +|============== +[[command-accept]] +=== command-accept +Execute the command currently in the commandline. + +[[command-history-next]] +=== command-history-next +Go forward in the commandline history. + +[[command-history-prev]] +=== command-history-prev +Go back in the commandline history. + +[[completion-item-next]] +=== completion-item-next +Select the next completion item. + +[[completion-item-prev]] +=== completion-item-prev +Select the previous completion item. + +[[enter-mode]] +=== enter-mode +Syntax: +:enter-mode 'mode'+ + +Enter a key mode. + +==== positional arguments +* +'mode'+: The mode to enter. + +[[follow-hint]] +=== follow-hint +Follow the currently selected hint. + +[[leave-mode]] +=== leave-mode +Leave the mode we're currently in. + +[[open-editor]] +=== open-editor +Open an external editor with the currently selected form field. + +The editor which should be launched can be configured via the `general -> editor` config option. + +[[prompt-accept]] +=== prompt-accept +Accept the current prompt. + +[[prompt-no]] +=== prompt-no +Answer no to a yes/no prompt. + +[[prompt-yes]] +=== prompt-yes +Answer yes to a yes/no prompt. + +[[rl-backward-char]] +=== rl-backward-char +Move back a character. + +This acts like readline's backward-char. + +[[rl-backward-delete-char]] +=== rl-backward-delete-char +Delete the character before the cursor. + +This acts like readline's backward-delete-char. + +[[rl-backward-word]] +=== rl-backward-word +Move back to the start of the current or previous word. + +This acts like readline's backward-word. + +[[rl-beginning-of-line]] +=== rl-beginning-of-line +Move to the start of the line. + +This acts like readline's beginning-of-line. + +[[rl-delete-char]] +=== rl-delete-char +Delete the character after the cursor. + +This acts like readline's delete-char. + +[[rl-end-of-line]] +=== rl-end-of-line +Move to the end of the line. + +This acts like readline's end-of-line. + +[[rl-forward-char]] +=== rl-forward-char +Move forward a character. + +This acts like readline's forward-char. + +[[rl-forward-word]] +=== rl-forward-word +Move forward to the end of the next word. + +This acts like readline's forward-word. + +[[rl-kill-line]] +=== rl-kill-line +Remove chars from the cursor to the end of the line. + +This acts like readline's kill-line. + +[[rl-kill-word]] +=== rl-kill-word +Remove chars from the cursor to the end of the current word. + +This acts like readline's kill-word. + +[[rl-unix-line-discard]] +=== rl-unix-line-discard +Remove chars backward from the cursor to the beginning of the line. + +This acts like readline's unix-line-discard. + +[[rl-unix-word-rubout]] +=== rl-unix-word-rubout +Remove chars from the cursor to the beginning of the word. + +This acts like readline's unix-word-rubout. + +[[rl-yank]] +=== rl-yank +Paste the most recently deleted text. + +This acts like readline's yank. + +[[scroll]] +=== scroll +Syntax: +:scroll 'dx' 'dy'+ + +Scroll the current tab by 'count * dx/dy'. + +==== positional arguments +* +'dx'+: How much to scroll in x-direction. +* +'dy'+: How much to scroll in x-direction. + +[[scroll-page]] +=== scroll-page +Syntax: +:scroll-page 'x' 'y'+ + +Scroll the frame page-wise. + +==== positional arguments +* +'x'+: How many pages to scroll to the right. +* +'y'+: How many pages to scroll down. + +[[scroll-perc]] +=== scroll-perc +Syntax: +:scroll-perc [*--horizontal*] ['perc']+ + +Scroll to a specific percentage of the page. + +The percentage can be given either as argument or as count. If no percentage is given, the page is scrolled to the end. + +==== positional arguments +* +'perc'+: Percentage to scroll. + +==== optional arguments +* +*-x*+, +*--horizontal*+: Whether to scroll horizontally. + +[[search-next]] +=== search-next +Continue the search to the ([count]th) next term. + +[[search-prev]] +=== search-prev +Continue the search to the ([count]th) previous term. + + +== Debugging commands +These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag. + +.Quick reference +[options="header",width="75%",cols="25%,75%"] +|============== +|Command|Description +|<>|Print a list of all objects to the debug log. +|<>|Print a list of all widgets to debug log. +|<>|Print LRU cache stats. +|<>|Show the debugging console. +|<>|Crash for debugging purposes. +|<>|Evaluate a python string and display the results as a webpage. +|============== +[[debug-all-objects]] +=== debug-all-objects +Print a list of all objects to the debug log. + +[[debug-all-widgets]] +=== debug-all-widgets +Print a list of all widgets to debug log. + +[[debug-cache-stats]] +=== debug-cache-stats +Print LRU cache stats. + +[[debug-console]] +=== debug-console +Show the debugging console. + +[[debug-crash]] +=== debug-crash +Syntax: +:debug-crash ['typ']+ + +Crash for debugging purposes. + +==== positional arguments +* +'typ'+: either 'exception' or 'segfault'. + +[[debug-pyeval]] +=== debug-pyeval +Syntax: +:debug-pyeval 's'+ + +Evaluate a python string and display the results as a webpage. + +==== positional arguments +* +'s'+: The string to evaluate. + diff --git a/doc/settings.asciidoc b/doc/settings.asciidoc new file mode 100644 index 000000000..e0acf4553 --- /dev/null +++ b/doc/settings.asciidoc @@ -0,0 +1,1255 @@ += Settings + +.Quick reference for section ``general'' +[options="header",width="75%",cols="25%,75%"] +|============== +|Setting|Description +|<>|Whether to find text on a page case-insensitively. +|<>|Whether to wrap finding text to the top when arriving at the end. +|<>|The default page(s) to open at the start, separated by commas. +|<>|Whether to start a search when something else than a URL is entered. +|<>|Whether to save the config automatically on quit. +|<>|The editor (and arguments) to use for the `open-editor` command. +|<>|Encoding to use for editor. +|<>|Do not record visited pages in the history or store web page icons. +|<>|Enable extra tools for Web developers. +|<>|Whether the background color and images are also drawn when the page is printed. +|<>|Whether load requests should be monitored for cross-site scripting attempts. +|<>|Enable workarounds for broken sites. +|<>|Default encoding to use for websites. +|============== + +.Quick reference for section ``ui'' +[options="header",width="75%",cols="25%,75%"] +|============== +|Setting|Description +|<>|The available zoom levels, separated by commas. +|<>|The default zoom level. +|<>|Time (in ms) to show messages in the statusbar for. +|<>|Whether to confirm quitting the application. +|<>|Whether to display javascript statusbar messages. +|<>|Whether the zoom factor on a frame applies only to the text or to all content. +|<>|Whether to expand each subframe to its contents. +|<>|User stylesheet to use. +|<>|Set the CSS media type. +|============== + +.Quick reference for section ``network'' +[options="header",width="75%",cols="25%,75%"] +|============== +|Setting|Description +|<>|Value to send in the `DNT` header. +|<>|Value to send in the `accept-language` header. +|<>|User agent to send. Empty to send the default. +|<>|The proxy to use. +|<>|Whether to validate SSL handshakes. +|<>|Whether to try to pre-fetch DNS entries to speed up browsing. +|============== + +.Quick reference for section ``completion'' +[options="header",width="75%",cols="25%,75%"] +|============== +|Setting|Description +|<>|Whether to show the autocompletion window. +|<>|The height of the completion, in px or as percentage of the window. +|<>|How many commands to save in the history. +|<>|Whether to move on to the next part when there's only one possible completion left. +|<>|Whether to shrink the completion to be smaller than the configured size if there are no scrollbars. +|============== + +.Quick reference for section ``input'' +[options="header",width="75%",cols="25%,75%"] +|============== +|Setting|Description +|<>|Timeout for ambiguous keybindings. +|<>|Whether to switch to insert mode when clicking flash and other plugins. +|<>|Whether to leave insert mode if a non-editable element is clicked. +|<>|Whether to automatically enter insert mode if an editable element is focused after page load. +|<>|Whether to forward unbound keys to the webview in normal mode. +|<>|Enables or disables the Spatial Navigation feature +|<>|Whether hyperlinks should be included in the keyboard focus chain. +|============== + +.Quick reference for section ``tabs'' +[options="header",width="75%",cols="25%,75%"] +|============== +|Setting|Description +|<>|Whether to open new tabs (middleclick/ctrl+click) in background. +|<>|Which tab to select when the focused tab is removed. +|<>|How new tabs are positioned. +|<>|How new tabs opened explicitely are positioned. +|<>|Behaviour when the last tab is closed. +|<>|Whether to wrap when changing tabs. +|<>|Whether tabs should be movable. +|<>|On which mouse button to close tabs. +|<>|The position of the tab bar. +|<>|Whether to show favicons in the tab bar. +|<>|The width of the tab bar if it's vertical, in px or as percentage of the window. +|<>|Width of the progress indicator (0 to disable). +|<>|Spacing between tab edge and indicator. +|============== + +.Quick reference for section ``storage'' +[options="header",width="75%",cols="25%,75%"] +|============== +|Setting|Description +|<>|The directory to save downloads to. An empty value selects a sensible os-specific default. +|<>|The maximum number of pages to hold in the memory page cache. +|<>|The capacities for the memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity. +|<>|Default quota for new offline storage databases. +|<>|Quota for the offline web application cache. +|<>|Whether support for the HTML 5 offline storage feature is enabled. +|<>|Whether support for the HTML 5 web application cache feature is enabled. +|<>|Whether support for the HTML 5 local storage feature is enabled. +|<>|Size of the HTTP network cache. +|============== + +.Quick reference for section ``permissions'' +[options="header",width="75%",cols="25%,75%"] +|============== +|Setting|Description +|<>|Whether images are automatically loaded in web pages. +|<>|Enables or disables the running of JavaScript programs. +|<>|Enables or disables plugins in Web pages. +|<>|Whether JavaScript programs can open new windows. +|<>|Whether JavaScript programs can close windows. +|<>|Whether JavaScript programs can read or write to the clipboard. +|<>|Whether locally loaded documents are allowed to access remote urls. +|<>|Whether locally loaded documents are allowed to access other local urls. +|<>|Whether to accept cookies. +|<>|Whether to store cookies. +|============== + +.Quick reference for section ``hints'' +[options="header",width="75%",cols="25%,75%"] +|============== +|Setting|Description +|<>|CSS border value for hints. +|<>|Opacity for hints. +|<>|Mode to use for hints. +|<>|Chars used for hint strings. +|<>|Whether to auto-follow a hint if there's only one left. +|<>|A comma-separated list of regexes to use for 'next' links. +|<>|A comma-separated list of regexes to use for 'prev' links. +|============== + +.Quick reference for section ``colors'' +[options="header",width="75%",cols="25%,75%"] +|============== +|Setting|Description +|<>|Text color of the completion widget. +|<>|Background color of the completion widget. +|<>|Background color of completion widget items. +|<>|Foreground color of completion widget category headers. +|<>|Background color of the completion widget category headers. +|<>|Top border color of the completion widget category headers. +|<>|Bottom border color of the completion widget category headers. +|<>|Foreground color of the selected completion item. +|<>|Background color of the selected completion item. +|<>|Top border color of the completion widget category headers. +|<>|Bottom border color of the selected completion item. +|<>|Foreground color of the matched text in the completion. +|<>|Foreground color of the statusbar. +|<>|Foreground color of the statusbar. +|<>|Background color of the statusbar if there was an error. +|<>|Background color of the statusbar if there is a prompt. +|<>|Background color of the statusbar in insert mode. +|<>|Background color of the progress bar. +|<>|Default foreground color of the URL in the statusbar. +|<>|Foreground color of the URL in the statusbar on successful load. +|<>|Foreground color of the URL in the statusbar on error. +|<>|Foreground color of the URL in the statusbar when there's a warning. +|<>|Foreground color of the URL in the statusbar for hovered links. +|<>|Foreground color of tabs. +|<>|Background color of unselected odd tabs. +|<>|Background color of unselected even tabs. +|<>|Background color of selected tabs. +|<>|Background color of the tabbar. +|<>|Color gradient start for the tab indicator. +|<>|Color gradient end for the tab indicator. +|<>|Color for the tab indicator on errors.. +|<>|Color gradient interpolation system for the tab indicator. +|<>|Color for the tab seperator. +|<>|Font color for hints. +|<>|Font color for the matched part of hints. +|<>|Background color for hints. +|<>|Foreground color for downloads. +|<>|Background color for the download bar. +|<>|Color gradient start for downloads. +|<>|Color gradient end for downloads. +|<>|Color gradient interpolation system for downloads. +|============== + +.Quick reference for section ``fonts'' +[options="header",width="75%",cols="25%,75%"] +|============== +|Setting|Description +|<>|Default monospace fonts. +|<>|Font used in the completion widget. +|<>|Font used in the tabbar. +|<>|Font used in the statusbar. +|<>|Font used for the downloadbar. +|<>|Font used for the hints. +|<>|Font used for the debugging console. +|<>|Font family for standard fonts. +|<>|Font family for fixed fonts. +|<>|Font family for serif fonts. +|<>|Font family for sans-serif fonts. +|<>|Font family for cursive fonts. +|<>|Font family for fantasy fonts. +|<>|The hard minimum font size. +|<>|The minimum logical font size that is applied when zooming out. +|<>|The default font size for regular text. +|<>|The default font size for fixed-pitch text. +|============== + +== general +General/miscellaneous options. + +[[general-ignore-case]] +=== ignore-case +Whether to find text on a page case-insensitively. + +Default: +pass:[smart]+ + +[[general-wrap-search]] +=== wrap-search +Whether to wrap finding text to the top when arriving at the end. + +Default: +pass:[true]+ + +[[general-startpage]] +=== startpage +The default page(s) to open at the start, separated by commas. + +Default: +pass:[http://www.duckduckgo.com]+ + +[[general-auto-search]] +=== auto-search +Whether to start a search when something else than a URL is entered. + +Valid values: + + * +naive+: Use simple/naive check. + * +dns+: Use DNS requests (might be slow!). + * +false+: Never search automatically. + +Default: +pass:[naive]+ + +[[general-auto-save-config]] +=== auto-save-config +Whether to save the config automatically on quit. + +Default: +pass:[true]+ + +[[general-editor]] +=== editor +The editor (and arguments) to use for the `open-editor` command. + +Use `{}` for the filename. The value gets split like in a shell, so you can use `"` or `'` to quote arguments. + +Default: +pass:[gvim -f "{}"]+ + +[[general-editor-encoding]] +=== editor-encoding +Encoding to use for editor. + +Default: +pass:[utf-8]+ + +[[general-private-browsing]] +=== private-browsing +Do not record visited pages in the history or store web page icons. + +Default: +pass:[false]+ + +[[general-developer-extras]] +=== developer-extras +Enable extra tools for Web developers. + +This needs to be enabled for `:inspector` to work and also adds an _Inspect_ entry to the context menu. + +Default: +pass:[false]+ + +[[general-print-element-backgrounds]] +=== print-element-backgrounds +Whether the background color and images are also drawn when the page is printed. + +Default: +pass:[true]+ + +[[general-xss-auditing]] +=== xss-auditing +Whether load requests should be monitored for cross-site scripting attempts. + +Suspicious scripts will be blocked and reported in the inspector's JavaScript console. Enabling this feature might have an impact on performance. + +Default: +pass:[false]+ + +[[general-site-specific-quirks]] +=== site-specific-quirks +Enable workarounds for broken sites. + +Default: +pass:[true]+ + +[[general-default-encoding]] +=== default-encoding +Default encoding to use for websites. + +The encoding must be a string describing an encoding such as _utf-8_, _iso-8859-1_, etc. If left empty a default value will be used. + +Default: empty + +== ui +General options related to the user interface. + +[[ui-zoom-levels]] +=== zoom-levels +The available zoom levels, separated by commas. + +Default: +pass:[25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,200%,250%,300%,400%,500%]+ + +[[ui-default-zoom]] +=== default-zoom +The default zoom level. + +Default: +pass:[100%]+ + +[[ui-message-timeout]] +=== message-timeout +Time (in ms) to show messages in the statusbar for. + +Default: +pass:[2000]+ + +[[ui-confirm-quit]] +=== confirm-quit +Whether to confirm quitting the application. + +Valid values: + + * +always+: Always show a confirmation. + * +multiple-tabs+: Show a confirmation if multiple tabs are opened. + * +never+: Never show a confirmation. + +Default: +pass:[never]+ + +[[ui-display-statusbar-messages]] +=== display-statusbar-messages +Whether to display javascript statusbar messages. + +Default: +pass:[false]+ + +[[ui-zoom-text-only]] +=== zoom-text-only +Whether the zoom factor on a frame applies only to the text or to all content. + +Default: +pass:[false]+ + +[[ui-frame-flattening]] +=== frame-flattening +Whether to expand each subframe to its contents. + +This will flatten all the frames to become one scrollable page. + +Default: +pass:[false]+ + +[[ui-user-stylesheet]] +=== user-stylesheet +User stylesheet to use. + +Default: empty + +[[ui-css-media-type]] +=== css-media-type +Set the CSS media type. + +Default: empty + +== network +Settings related to the network. + +[[network-do-not-track]] +=== do-not-track +Value to send in the `DNT` header. + +Default: +pass:[true]+ + +[[network-accept-language]] +=== accept-language +Value to send in the `accept-language` header. + +Default: +pass:[en-US,en]+ + +[[network-user-agent]] +=== user-agent +User agent to send. Empty to send the default. + +Default: empty + +[[network-proxy]] +=== proxy +The proxy to use. + +In addition to the listed values, you can use a `socks://...` or `http://...` URL. + +Valid values: + + * +system+: Use the system wide proxy. + * +none+: Don't use any proxy + +Default: +pass:[system]+ + +[[network-ssl-strict]] +=== ssl-strict +Whether to validate SSL handshakes. + +Default: +pass:[true]+ + +[[network-dns-prefetch]] +=== dns-prefetch +Whether to try to pre-fetch DNS entries to speed up browsing. + +Default: +pass:[true]+ + +== completion +Options related to completion and command history. + +[[completion-show]] +=== show +Whether to show the autocompletion window. + +Default: +pass:[true]+ + +[[completion-height]] +=== height +The height of the completion, in px or as percentage of the window. + +Default: +pass:[50%]+ + +[[completion-history-length]] +=== history-length +How many commands to save in the history. + +0: no history / -1: unlimited + +Default: +pass:[100]+ + +[[completion-quick-complete]] +=== quick-complete +Whether to move on to the next part when there's only one possible completion left. + +Default: +pass:[true]+ + +[[completion-shrink]] +=== shrink +Whether to shrink the completion to be smaller than the configured size if there are no scrollbars. + +Default: +pass:[false]+ + +== input +Options related to input modes. + +[[input-timeout]] +=== timeout +Timeout for ambiguous keybindings. + +Default: +pass:[500]+ + +[[input-insert-mode-on-plugins]] +=== insert-mode-on-plugins +Whether to switch to insert mode when clicking flash and other plugins. + +Default: +pass:[false]+ + +[[input-auto-leave-insert-mode]] +=== auto-leave-insert-mode +Whether to leave insert mode if a non-editable element is clicked. + +Default: +pass:[true]+ + +[[input-auto-insert-mode]] +=== auto-insert-mode +Whether to automatically enter insert mode if an editable element is focused after page load. + +Default: +pass:[false]+ + +[[input-forward-unbound-keys]] +=== forward-unbound-keys +Whether to forward unbound keys to the webview in normal mode. + +Valid values: + + * +all+: Forward all unbound keys. + * +auto+: Forward unbound non-alphanumeric keys. + * +none+: Don't forward any keys. + +Default: +pass:[auto]+ + +[[input-spatial-navigation]] +=== spatial-navigation +Enables or disables the Spatial Navigation feature + +Spatial navigation consists in the ability to navigate between focusable elements in a Web page, such as hyperlinks and form controls, by using Left, Right, Up and Down arrow keys. For example, if a user presses the Right key, heuristics determine whether there is an element he might be trying to reach towards the right and which element he probably wants. + +Default: +pass:[false]+ + +[[input-links-included-in-focus-chain]] +=== links-included-in-focus-chain +Whether hyperlinks should be included in the keyboard focus chain. + +Default: +pass:[true]+ + +== tabs +Configuration of the tab bar. + +[[tabs-background-tabs]] +=== background-tabs +Whether to open new tabs (middleclick/ctrl+click) in background. + +Default: +pass:[false]+ + +[[tabs-select-on-remove]] +=== select-on-remove +Which tab to select when the focused tab is removed. + +Valid values: + + * +left+: Select the tab on the left. + * +right+: Select the tab on the right. + * +previous+: Select the previously selected tab. + +Default: +pass:[right]+ + +[[tabs-new-tab-position]] +=== new-tab-position +How new tabs are positioned. + +Valid values: + + * +left+: On the left of the current tab. + * +right+: On the right of the current tab. + * +first+: At the left end. + * +last+: At the right end. + +Default: +pass:[right]+ + +[[tabs-new-tab-position-explicit]] +=== new-tab-position-explicit +How new tabs opened explicitely are positioned. + +Valid values: + + * +left+: On the left of the current tab. + * +right+: On the right of the current tab. + * +first+: At the left end. + * +last+: At the right end. + +Default: +pass:[last]+ + +[[tabs-last-close]] +=== last-close +Behaviour when the last tab is closed. + +Valid values: + + * +ignore+: Don't do anything. + * +blank+: Load a blank page. + * +quit+: Quit qutebrowser. + +Default: +pass:[ignore]+ + +[[tabs-wrap]] +=== wrap +Whether to wrap when changing tabs. + +Default: +pass:[true]+ + +[[tabs-movable]] +=== movable +Whether tabs should be movable. + +Default: +pass:[true]+ + +[[tabs-close-mouse-button]] +=== close-mouse-button +On which mouse button to close tabs. + +Valid values: + + * +right+: Close tabs on right-click. + * +middle+: Close tabs on middle-click. + * +none+: Don't close tabs using the mouse. + +Default: +pass:[middle]+ + +[[tabs-position]] +=== position +The position of the tab bar. + +Valid values: + + * +north+ + * +south+ + * +east+ + * +west+ + +Default: +pass:[north]+ + +[[tabs-show-favicons]] +=== show-favicons +Whether to show favicons in the tab bar. + +Default: +pass:[true]+ + +[[tabs-width]] +=== width +The width of the tab bar if it's vertical, in px or as percentage of the window. + +Default: +pass:[20%]+ + +[[tabs-indicator-width]] +=== indicator-width +Width of the progress indicator (0 to disable). + +Default: +pass:[3]+ + +[[tabs-indicator-space]] +=== indicator-space +Spacing between tab edge and indicator. + +Default: +pass:[3]+ + +== storage +Settings related to cache and storage. + +[[storage-download-directory]] +=== download-directory +The directory to save downloads to. An empty value selects a sensible os-specific default. + +Default: empty + +[[storage-maximum-pages-in-cache]] +=== maximum-pages-in-cache +The maximum number of pages to hold in the memory page cache. + +The Page Cache allows for a nicer user experience when navigating forth or back to pages in the forward/back history, by pausing and resuming up to _n_ pages. + +For more information about the feature, please refer to: http://webkit.org/blog/427/webkit-page-cache-i-the-basics/ + +Default: empty + +[[storage-object-cache-capacities]] +=== object-cache-capacities +The capacities for the memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity. + +The _cacheMinDeadCapacity_ specifies the minimum number of bytes that dead objects should consume when the cache is under pressure. + +_cacheMaxDead_ is the maximum number of bytes that dead objects should consume when the cache is *not* under pressure. + +_totalCapacity_ specifies the maximum number of bytes that the cache should consume *overall*. + +Default: empty + +[[storage-offline-storage-default-quota]] +=== offline-storage-default-quota +Default quota for new offline storage databases. + +Default: empty + +[[storage-offline-web-application-cache-quota]] +=== offline-web-application-cache-quota +Quota for the offline web application cache. + +Default: empty + +[[storage-offline-storage-database]] +=== offline-storage-database +Whether support for the HTML 5 offline storage feature is enabled. + +Default: +pass:[true]+ + +[[storage-offline-web-application-storage]] +=== offline-web-application-storage +Whether support for the HTML 5 web application cache feature is enabled. + +An application cache acts like an HTTP cache in some sense. For documents that use the application cache via JavaScript, the loader engine will first ask the application cache for the contents, before hitting the network. + +The feature is described in details at: http://dev.w3.org/html5/spec/Overview.html#appcache + +Default: +pass:[true]+ + +[[storage-local-storage]] +=== local-storage +Whether support for the HTML 5 local storage feature is enabled. + +Default: +pass:[true]+ + +[[storage-cache-size]] +=== cache-size +Size of the HTTP network cache. + +Default: +pass:[52428800]+ + +== permissions +Loaded plugins/scripts and allowed actions. + +[[permissions-allow-images]] +=== allow-images +Whether images are automatically loaded in web pages. + +Default: +pass:[true]+ + +[[permissions-allow-javascript]] +=== allow-javascript +Enables or disables the running of JavaScript programs. + +Default: +pass:[true]+ + +[[permissions-allow-plugins]] +=== allow-plugins +Enables or disables plugins in Web pages. + +Qt plugins with a mimetype such as "application/x-qt-plugin" are not affected by this setting. + +Default: +pass:[false]+ + +[[permissions-javascript-can-open-windows]] +=== javascript-can-open-windows +Whether JavaScript programs can open new windows. + +Default: +pass:[false]+ + +[[permissions-javascript-can-close-windows]] +=== javascript-can-close-windows +Whether JavaScript programs can close windows. + +Default: +pass:[false]+ + +[[permissions-javascript-can-access-clipboard]] +=== javascript-can-access-clipboard +Whether JavaScript programs can read or write to the clipboard. + +Default: +pass:[false]+ + +[[permissions-local-content-can-access-remote-urls]] +=== local-content-can-access-remote-urls +Whether locally loaded documents are allowed to access remote urls. + +Default: +pass:[false]+ + +[[permissions-local-content-can-access-file-urls]] +=== local-content-can-access-file-urls +Whether locally loaded documents are allowed to access other local urls. + +Default: +pass:[true]+ + +[[permissions-cookies-accept]] +=== cookies-accept +Whether to accept cookies. + +Valid values: + + * +default+: Default QtWebKit behaviour. + * +never+: Don't accept cookies at all. + +Default: +pass:[default]+ + +[[permissions-cookies-store]] +=== cookies-store +Whether to store cookies. + +Default: +pass:[true]+ + +== hints +Hinting settings. + +[[hints-border]] +=== border +CSS border value for hints. + +Default: +pass:[1px solid #E3BE23]+ + +[[hints-opacity]] +=== opacity +Opacity for hints. + +Default: +pass:[0.7]+ + +[[hints-mode]] +=== mode +Mode to use for hints. + +Valid values: + + * +number+: Use numeric hints. + * +letter+: Use the chars in the hints -> chars setting. + +Default: +pass:[letter]+ + +[[hints-chars]] +=== chars +Chars used for hint strings. + +Default: +pass:[asdfghjkl]+ + +[[hints-auto-follow]] +=== auto-follow +Whether to auto-follow a hint if there's only one left. + +Default: +pass:[true]+ + +[[hints-next-regexes]] +=== next-regexes +A comma-separated list of regexes to use for 'next' links. + +Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b]+ + +[[hints-prev-regexes]] +=== prev-regexes +A comma-separated list of regexes to use for 'prev' links. + +Default: +pass:[\bprev(ious)?\b,\bback\b,\bolder\b,\b[<←≪]\b,\b(<<|«)\b]+ + +== searchengines +Definitions of search engines which can be used via the address bar. +The searchengine named `DEFAULT` is used when `general -> auto-search` is true and something else than a URL was entered to be opened. Other search engines can be used via the bang-syntax, e.g. `:open qutebrowser !google`. The string `{}` will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs. + +== keybind +Bindings from a key(chain) to a command. +For special keys (can't be part of a keychain), enclose them in `<`...`>`. For modifiers, you can use either `-` or `+` as delimiters, and these names: + + * Control: `Control`, `Ctrl` + * Meta: `Meta`, `Windows`, `Mod4` + * Alt: `Alt`, `Mod1` + * Shift: `Shift` + +For simple keys (no `<>`-signs), a capital letter means the key is pressed with Shift. For special keys (with `<>`-signs), you need to explicitely add `Shift-` to match a key pressed with shift. You can bind multiple commands by separating them with `;;`. + +== keybind.insert +Keybindings for insert mode. +Since normal keypresses are passed through, only special keys are supported in this mode. +Useful hidden commands to map in this section: + + * `open-editor`: Open a texteditor with the focused field. + * `leave-mode`: Leave the command mode. + +== keybind.hint +Keybindings for hint mode. +Since normal keypresses are passed through, only special keys are supported in this mode. +Useful hidden commands to map in this section: + + * `follow-hint`: Follow the currently selected hint. + * `leave-mode`: Leave the command mode. + +== keybind.passthrough +Keybindings for passthrough mode. +Since normal keypresses are passed through, only special keys are supported in this mode. +Useful hidden commands to map in this section: + + * `leave-mode`: Leave the passthrough mode. + +== keybind.command +Keybindings for command mode. +Since normal keypresses are passed through, only special keys are supported in this mode. +Useful hidden commands to map in this section: + + * `command-history-prev`: Switch to previous command in history. + * `command-history-next`: Switch to next command in history. + * `completion-item-prev`: Select previous item in completion. + * `completion-item-next`: Select next item in completion. + * `command-accept`: Execute the command currently in the commandline. + * `leave-mode`: Leave the command mode. + +== keybind.prompt +Keybindings for prompts in the status line. +You can bind normal keys in this mode, but they will be only active when a yes/no-prompt is asked. For other prompt modes, you can only bind special keys. +Useful hidden commands to map in this section: + + * `prompt-accept`: Confirm the entered value. + * `prompt-yes`: Answer yes to a yes/no question. + * `prompt-no`: Answer no to a yes/no question. + * `leave-mode`: Leave the prompt mode. + +== aliases +Aliases for commands. +By default, no aliases are defined. Example which adds a new command `:qtb` to open qutebrowsers website: + +`qtb = open http://www.qutebrowser.org/` + +== colors +Colors used in the UI. +A value can be in one of the following format: + + * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` + * A SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. + * transparent (no color) + * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) + * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) + * A gradient as explained in http://qt-project.org/doc/qt-4.8/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''. + +The `hints.*` values are a special case as they're real CSS colors, not Qt-CSS colors. There, for a gradient, you need to use `-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-css-gradients/[the WebKit documentation]. + +[[colors-completion.fg]] +=== completion.fg +Text color of the completion widget. + +Default: +pass:[white]+ + +[[colors-completion.bg]] +=== completion.bg +Background color of the completion widget. + +Default: +pass:[#333333]+ + +[[colors-completion.item.bg]] +=== completion.item.bg +Background color of completion widget items. + +Default: +pass:[${completion.bg}]+ + +[[colors-completion.category.fg]] +=== completion.category.fg +Foreground color of completion widget category headers. + +Default: +pass:[white]+ + +[[colors-completion.category.bg]] +=== completion.category.bg +Background color of the completion widget category headers. + +Default: +pass:[qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #888888, stop:1 #505050)]+ + +[[colors-completion.category.border.top]] +=== completion.category.border.top +Top border color of the completion widget category headers. + +Default: +pass:[black]+ + +[[colors-completion.category.border.bottom]] +=== completion.category.border.bottom +Bottom border color of the completion widget category headers. + +Default: +pass:[${completion.category.border.top}]+ + +[[colors-completion.item.selected.fg]] +=== completion.item.selected.fg +Foreground color of the selected completion item. + +Default: +pass:[black]+ + +[[colors-completion.item.selected.bg]] +=== completion.item.selected.bg +Background color of the selected completion item. + +Default: +pass:[#e8c000]+ + +[[colors-completion.item.selected.border.top]] +=== completion.item.selected.border.top +Top border color of the completion widget category headers. + +Default: +pass:[#bbbb00]+ + +[[colors-completion.item.selected.border.bottom]] +=== completion.item.selected.border.bottom +Bottom border color of the selected completion item. + +Default: +pass:[${completion.item.selected.border.top}]+ + +[[colors-completion.match.fg]] +=== completion.match.fg +Foreground color of the matched text in the completion. + +Default: +pass:[#ff4444]+ + +[[colors-statusbar.bg]] +=== statusbar.bg +Foreground color of the statusbar. + +Default: +pass:[black]+ + +[[colors-statusbar.fg]] +=== statusbar.fg +Foreground color of the statusbar. + +Default: +pass:[white]+ + +[[colors-statusbar.bg.error]] +=== statusbar.bg.error +Background color of the statusbar if there was an error. + +Default: +pass:[red]+ + +[[colors-statusbar.bg.prompt]] +=== statusbar.bg.prompt +Background color of the statusbar if there is a prompt. + +Default: +pass:[darkblue]+ + +[[colors-statusbar.bg.insert]] +=== statusbar.bg.insert +Background color of the statusbar in insert mode. + +Default: +pass:[darkgreen]+ + +[[colors-statusbar.progress.bg]] +=== statusbar.progress.bg +Background color of the progress bar. + +Default: +pass:[white]+ + +[[colors-statusbar.url.fg]] +=== statusbar.url.fg +Default foreground color of the URL in the statusbar. + +Default: +pass:[${statusbar.fg}]+ + +[[colors-statusbar.url.fg.success]] +=== statusbar.url.fg.success +Foreground color of the URL in the statusbar on successful load. + +Default: +pass:[lime]+ + +[[colors-statusbar.url.fg.error]] +=== statusbar.url.fg.error +Foreground color of the URL in the statusbar on error. + +Default: +pass:[orange]+ + +[[colors-statusbar.url.fg.warn]] +=== statusbar.url.fg.warn +Foreground color of the URL in the statusbar when there's a warning. + +Default: +pass:[yellow]+ + +[[colors-statusbar.url.fg.hover]] +=== statusbar.url.fg.hover +Foreground color of the URL in the statusbar for hovered links. + +Default: +pass:[aqua]+ + +[[colors-tab.fg]] +=== tab.fg +Foreground color of tabs. + +Default: +pass:[white]+ + +[[colors-tab.bg.odd]] +=== tab.bg.odd +Background color of unselected odd tabs. + +Default: +pass:[grey]+ + +[[colors-tab.bg.even]] +=== tab.bg.even +Background color of unselected even tabs. + +Default: +pass:[darkgrey]+ + +[[colors-tab.bg.selected]] +=== tab.bg.selected +Background color of selected tabs. + +Default: +pass:[black]+ + +[[colors-tab.bg.bar]] +=== tab.bg.bar +Background color of the tabbar. + +Default: +pass:[#555555]+ + +[[colors-tab.indicator.start]] +=== tab.indicator.start +Color gradient start for the tab indicator. + +Default: +pass:[#0000aa]+ + +[[colors-tab.indicator.stop]] +=== tab.indicator.stop +Color gradient end for the tab indicator. + +Default: +pass:[#00aa00]+ + +[[colors-tab.indicator.error]] +=== tab.indicator.error +Color for the tab indicator on errors.. + +Default: +pass:[#ff0000]+ + +[[colors-tab.indicator.system]] +=== tab.indicator.system +Color gradient interpolation system for the tab indicator. + +Valid values: + + * +rgb+: Interpolate in the RGB color system. + * +hsv+: Interpolate in the HSV color system. + * +hsl+: Interpolate in the HSL color system. + +Default: +pass:[rgb]+ + +[[colors-tab.seperator]] +=== tab.seperator +Color for the tab seperator. + +Default: +pass:[#555555]+ + +[[colors-hints.fg]] +=== hints.fg +Font color for hints. + +Default: +pass:[black]+ + +[[colors-hints.fg.match]] +=== hints.fg.match +Font color for the matched part of hints. + +Default: +pass:[green]+ + +[[colors-hints.bg]] +=== hints.bg +Background color for hints. + +Default: +pass:[-webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542))]+ + +[[colors-downloads.fg]] +=== downloads.fg +Foreground color for downloads. + +Default: +pass:[#ffffff]+ + +[[colors-downloads.bg.bar]] +=== downloads.bg.bar +Background color for the download bar. + +Default: +pass:[black]+ + +[[colors-downloads.bg.start]] +=== downloads.bg.start +Color gradient start for downloads. + +Default: +pass:[#0000aa]+ + +[[colors-downloads.bg.stop]] +=== downloads.bg.stop +Color gradient end for downloads. + +Default: +pass:[#00aa00]+ + +[[colors-downloads.bg.system]] +=== downloads.bg.system +Color gradient interpolation system for downloads. + +Valid values: + + * +rgb+: Interpolate in the RGB color system. + * +hsv+: Interpolate in the HSV color system. + * +hsl+: Interpolate in the HSL color system. + +Default: +pass:[rgb]+ + +== fonts +Fonts used for the UI, with optional style/weight/size. + + * Style: `normal`/`italic`/`oblique` + * Weight: `normal`, `bold`, `100`..`900` + * Size: _number_ `px`/`pt` + +[[fonts-_monospace]] +=== _monospace +Default monospace fonts. + +Default: +pass:[Terminus, Monospace, "DejaVu Sans Mono", Consolas, Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Liberation Mono", "Courier New", Courier, monospace, Fixed, Terminal]+ + +[[fonts-completion]] +=== completion +Font used in the completion widget. + +Default: +pass:[8pt ${_monospace}]+ + +[[fonts-tabbar]] +=== tabbar +Font used in the tabbar. + +Default: +pass:[8pt ${_monospace}]+ + +[[fonts-statusbar]] +=== statusbar +Font used in the statusbar. + +Default: +pass:[8pt ${_monospace}]+ + +[[fonts-downloads]] +=== downloads +Font used for the downloadbar. + +Default: +pass:[8pt ${_monospace}]+ + +[[fonts-hints]] +=== hints +Font used for the hints. + +Default: +pass:[bold 12px Monospace]+ + +[[fonts-debug-console]] +=== debug-console +Font used for the debugging console. + +Default: +pass:[8pt ${_monospace}]+ + +[[fonts-web-family-standard]] +=== web-family-standard +Font family for standard fonts. + +Default: empty + +[[fonts-web-family-fixed]] +=== web-family-fixed +Font family for fixed fonts. + +Default: empty + +[[fonts-web-family-serif]] +=== web-family-serif +Font family for serif fonts. + +Default: empty + +[[fonts-web-family-sans-serif]] +=== web-family-sans-serif +Font family for sans-serif fonts. + +Default: empty + +[[fonts-web-family-cursive]] +=== web-family-cursive +Font family for cursive fonts. + +Default: empty + +[[fonts-web-family-fantasy]] +=== web-family-fantasy +Font family for fantasy fonts. + +Default: empty + +[[fonts-web-size-minimum]] +=== web-size-minimum +The hard minimum font size. + +Default: empty + +[[fonts-web-size-minimum-logical]] +=== web-size-minimum-logical +The minimum logical font size that is applied when zooming out. + +Default: empty + +[[fonts-web-size-default]] +=== web-size-default +The default font size for regular text. + +Default: empty + +[[fonts-web-size-default-fixed]] +=== web-size-default-fixed +The default font size for fixed-pitch text. + +Default: empty diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index 01e44e79e..884582a4b 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -55,7 +55,7 @@ class HelpAction(argparse.Action): def __call__(self, parser, _namespace, _values, _option_string=None): QCoreApplication.instance().mainwindow.tabs.tabopen( - QUrl('qute:help/commands.html#{}'.format(parser.name))) + QUrl('qute://help/commands.html#{}'.format(parser.name))) parser.exit() diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py index 9138449c9..a6046def9 100644 --- a/qutebrowser/network/qutescheme.py +++ b/qutebrowser/network/qutescheme.py @@ -54,19 +54,25 @@ class QuteSchemeHandler(schemehandler.SchemeHandler): A QNetworkReply. """ path = request.url().path() + host = request.url().host() # An url like "qute:foo" is split as "scheme:path", not "scheme:host". - logutils.misc.debug("url: {}, path: {}".format( - request.url().toDisplayString(), path)) + logutils.misc.debug("url: {}, path: {}, host {}".format( + request.url().toDisplayString(), path, host)) try: handler = getattr(QuteHandlers, path) except AttributeError: - errorstr = "No handler found for {}!".format( - request.url().toDisplayString()) - return schemehandler.ErrorNetworkReply( - request, errorstr, QNetworkReply.ContentNotFoundError, - self.parent()) + try: + handler = getattr(QuteHandlers, host) + except AttributeError: + errorstr = "No handler found for {}!".format( + request.url().toDisplayString()) + return schemehandler.ErrorNetworkReply( + request, errorstr, QNetworkReply.ContentNotFoundError, + self.parent()) + else: + data = handler(request) else: - data = handler() + data = handler(request) return schemehandler.SpecialNetworkReply( request, data, 'text/html', self.parent()) @@ -76,14 +82,14 @@ class QuteHandlers: """Handlers for qute:... pages.""" @classmethod - def pyeval(cls): + def pyeval(cls, _request): """Handler for qute:pyeval. Return HTML content as bytes.""" html = jinja.env.get_template('pre.html').render( title='pyeval', content=pyeval_output) return html.encode('UTF-8', errors='xmlcharrefreplace') @classmethod - def version(cls): + def version(cls, _request): """Handler for qute:version. Return HTML content as bytes.""" html = jinja.env.get_template('version.html').render( title='Version info', version=version.version(), @@ -91,7 +97,7 @@ class QuteHandlers: return html.encode('UTF-8', errors='xmlcharrefreplace') @classmethod - def plainlog(cls): + def plainlog(cls, _request): """Handler for qute:plainlog. Return HTML content as bytes.""" if logutils.ram_handler is None: text = "Log output was disabled." @@ -102,7 +108,7 @@ class QuteHandlers: return html.encode('UTF-8', errors='xmlcharrefreplace') @classmethod - def log(cls): + def log(cls, _request): """Handler for qute:log. Return HTML content as bytes.""" if logutils.ram_handler is None: html_log = None @@ -113,6 +119,12 @@ class QuteHandlers: return html.encode('UTF-8', errors='xmlcharrefreplace') @classmethod - def gpl(cls): + def gpl(cls, _request): """Handler for qute:gpl. Return HTML content as bytes.""" return utils.read_file('html/COPYING.html').encode('ASCII') + + @classmethod + def help(cls, request): + """Handler for qute:help. Return HTML content as bytes.""" + path = 'html/doc/{}'.format(request.url().path()) + return utils.read_file(path).encode('ASCII') diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 3ee51bf2a..3000541bf 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -410,9 +410,9 @@ if __name__ == '__main__': regenerate_authors('README.asciidoc') asciidoc_files = [('doc/qutebrowser.1.asciidoc', None), ('doc/settings.asciidoc', - 'qutebrowser/doc/settings.html'), + 'qutebrowser/html/doc/settings.html'), ('doc/commands.asciidoc', - 'qutebrowser/doc/commands.html'), + 'qutebrowser/html/doc/commands.html'), ('README.asciidoc', None)] for src, dst in asciidoc_files: call_asciidoc(src, dst) From 5e6150e6658b4202d213d8c3af6d1aec9fa08942 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Sep 2014 06:57:22 +0200 Subject: [PATCH 28/89] Add a :help command. --- doc/commands.asciidoc | 14 ++++++++++ qutebrowser/browser/commands.py | 35 +++++++++++++++++++++++++ qutebrowser/models/completion.py | 43 ++++++++++++++++++++++++++++++- qutebrowser/network/qutescheme.py | 3 ++- qutebrowser/utils/completer.py | 8 +++--- qutebrowser/utils/usertypes.py | 3 ++- 6 files changed, 100 insertions(+), 6 deletions(-) diff --git a/doc/commands.asciidoc b/doc/commands.asciidoc index edbe9b05d..b533690ff 100644 --- a/doc/commands.asciidoc +++ b/doc/commands.asciidoc @@ -10,6 +10,7 @@ |<>|Download the current page. |<>|Go forward in the history of the current tab. |<>|Get the value from a section/option. +|<>|Show help about a command or setting. |<>|Start hinting. |<>|Open main startpage in current tab. |<>|Toggle the web inspector. @@ -71,6 +72,19 @@ Get the value from a section/option. * +'sectname'+: The section where the option is in. * +'optname'+: The name of the option. +[[help]] +=== help +Syntax: +:help 'topic'+ + +Show help about a command or setting. + +==== positional arguments +* +'topic'+: The topic to show help for. + + - :__command__ for commands. + - __section__\->__option__ for settings. + + [[hint]] === hint Syntax: +:hint ['group'] ['target'] ['args']+ diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f4e730670..5a41fdb59 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -667,6 +667,41 @@ class CommandDispatcher: page = self._current_widget().page() self._tabs.download_get.emit(self._tabs.current_url(), page) + @cmdutils.register(instance='mainwindow.tabs.cmd', name='help', + completion=[usertypes.Completion.helptopic]) + def show_help(self, topic): + """Show help about a command or setting. + + Args: + topic: The topic to show help for. + + - :__command__ for commands. + - __section__\->__option__ for settings. + """ + if topic.startswith(':'): + command = topic[1:] + if command not in cmdutils.cmd_dict: + raise cmdexc.CommandError("Invalid command {}!".format( + command)) + path = 'commands.html#{}'.format(command) + elif '->' in topic: + parts = topic.split('->') + if len(parts) != 2: + raise cmdexc.CommandError("Invalid help topic {}!".format( + topic)) + try: + config.get(*parts) + except config.NoSectionError: + raise cmdexc.CommandError("Invalid section {}!".format( + parts[0])) + except config.NoOptionError: + raise cmdexc.CommandError("Invalid option {}!".format( + parts[1])) + path = 'settings.html#{}'.format(topic.replace('->', '-')) + else: + raise cmdexc.CommandError("Invalid help topic {}!".format(topic)) + self.openurl('qute://help/{}'.format(path)) + @cmdutils.register(instance='mainwindow.tabs.cmd', modes=[usertypes.KeyMode.insert], hide=True) diff --git a/qutebrowser/models/completion.py b/qutebrowser/models/completion.py index 6e09fca3d..39144ec86 100644 --- a/qutebrowser/models/completion.py +++ b/qutebrowser/models/completion.py @@ -58,7 +58,7 @@ class SettingOptionCompletionModel(basecompletion.BaseCompletionModel): sectdata = configdata.DATA[section] self._misc_items = {} self._section = section - for name, _ in sectdata.items(): + for name in sectdata.keys(): try: desc = sectdata.descriptions[name] except (KeyError, AttributeError): @@ -165,3 +165,44 @@ class CommandCompletionModel(basecompletion.BaseCompletionModel): cat = self.new_category("Commands") for (name, desc) in sorted(cmdlist): self.new_item(cat, name, desc) + + + +class HelpCompletionModel(basecompletion.BaseCompletionModel): + + """A CompletionModel filled with help topics.""" + + # pylint: disable=abstract-method + + def __init__(self, parent=None): + super().__init__(parent) + self._init_commands() + self._init_settings() + + def _init_commands(self): + assert cmdutils.cmd_dict + cmdlist = [] + for obj in set(cmdutils.cmd_dict.values()): + if obj.hide or (obj.debug and not + QCoreApplication.instance().args.debug): + pass + else: + cmdlist.append((':' + obj.name, obj.desc)) + cat = self.new_category("Commands") + for (name, desc) in sorted(cmdlist): + self.new_item(cat, name, desc) + + def _init_settings(self): + cat = self.new_category("Settings") + for sectname, sectdata in configdata.DATA.items(): + for optname in sectdata.keys(): + try: + desc = sectdata.descriptions[optname] + except (KeyError, AttributeError): + # Some stuff (especially ValueList items) don't have a + # description. + desc = "" + else: + desc = desc.splitlines()[0] + name = '{}->{}'.format(sectname, optname) + self.new_item(cat, name, desc) diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py index a6046def9..6d1f33412 100644 --- a/qutebrowser/network/qutescheme.py +++ b/qutebrowser/network/qutescheme.py @@ -127,4 +127,5 @@ class QuteHandlers: def help(cls, request): """Handler for qute:help. Return HTML content as bytes.""" path = 'html/doc/{}'.format(request.url().path()) - return utils.read_file(path).encode('ASCII') + return utils.read_file(path).encode('UTF-8', + errors='xmlcharrefreplace') diff --git a/qutebrowser/utils/completer.py b/qutebrowser/utils/completer.py index a70bbe5f2..d24d5c62c 100644 --- a/qutebrowser/utils/completer.py +++ b/qutebrowser/utils/completer.py @@ -57,13 +57,15 @@ class Completer(QObject): usertypes.Completion.option: {}, usertypes.Completion.value: {}, } - self._init_command_completion() + self._init_static_completions() self._init_setting_completions() - def _init_command_completion(self): - """Initialize the command completion model.""" + def _init_static_completions(self): + """Initialize the static completion models.""" self._models[usertypes.Completion.command] = CFM( models.CommandCompletionModel(self), self) + self._models[usertypes.Completion.helptopic] = CFM( + models.HelpCompletionModel(self), self) def _init_setting_completions(self): """Initialize setting completion models.""" diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 2337b4be0..d48e8b759 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -248,7 +248,8 @@ KeyMode = enum('KeyMode', 'normal', 'hint', 'command', 'yesno', 'prompt', # Available command completions -Completion = enum('Completion', 'command', 'section', 'option', 'value') +Completion = enum('Completion', 'command', 'section', 'option', 'value', + 'helptopic') class Question(QObject): From 381b06e967ed8b910ee45b11158b1ce7187d984f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Sep 2014 07:44:32 +0200 Subject: [PATCH 29/89] Fix lint --- .pylintrc | 2 +- qutebrowser/browser/commands.py | 2 +- qutebrowser/commands/command.py | 2 +- qutebrowser/models/completion.py | 3 ++- scripts/generate_doc.py | 20 +++++++++++++++----- scripts/run_checks.py | 2 +- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.pylintrc b/.pylintrc index 83ffc5fc1..f161de012 100644 --- a/.pylintrc +++ b/.pylintrc @@ -53,4 +53,4 @@ defining-attr-methods=__init__,__new__,setUp max-args=10 [TYPECHECK] -ignored-classes=WebElementWrapper +ignored-classes=WebElementWrapper,AnsiCodes diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 5a41fdb59..8bac8f0b0 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -670,7 +670,7 @@ class CommandDispatcher: @cmdutils.register(instance='mainwindow.tabs.cmd', name='help', completion=[usertypes.Completion.helptopic]) def show_help(self, topic): - """Show help about a command or setting. + r"""Show help about a command or setting. Args: topic: The topic to show help for. diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 996b879a4..490a3e558 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -54,7 +54,7 @@ class Command: completion, modes, not_modes, needs_js, is_debug, parser, type_conv, opt_args, pos_args): # I really don't know how to solve this in a better way, I tried. - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments,too-many-locals self.name = name self.split = split self.hide = hide diff --git a/qutebrowser/models/completion.py b/qutebrowser/models/completion.py index 39144ec86..e09622554 100644 --- a/qutebrowser/models/completion.py +++ b/qutebrowser/models/completion.py @@ -167,7 +167,6 @@ class CommandCompletionModel(basecompletion.BaseCompletionModel): self.new_item(cat, name, desc) - class HelpCompletionModel(basecompletion.BaseCompletionModel): """A CompletionModel filled with help topics.""" @@ -180,6 +179,7 @@ class HelpCompletionModel(basecompletion.BaseCompletionModel): self._init_settings() def _init_commands(self): + """Fill completion with :command entries.""" assert cmdutils.cmd_dict cmdlist = [] for obj in set(cmdutils.cmd_dict.values()): @@ -193,6 +193,7 @@ class HelpCompletionModel(basecompletion.BaseCompletionModel): self.new_item(cat, name, desc) def _init_settings(self): + """Fill completion with section->option entries.""" cat = self.new_category("Settings") for sectname, sectdata in configdata.DATA.items(): for optname in sectdata.keys(): diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 3000541bf..ba7c7b35d 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -35,10 +35,9 @@ import colorama as col sys.path.insert(0, os.getcwd()) -import qutebrowser # We import qutebrowser.app so all @cmdutils-register decorators are run. import qutebrowser.app -from qutebrowser import qutebrowser as qutequtebrowser +from qutebrowser import qutebrowser from qutebrowser.commands import cmdutils from qutebrowser.config import configdata from qutebrowser.utils import utils @@ -70,6 +69,7 @@ class UsageFormatter(argparse.HelpFormatter): result = "'{}'".format(default_metavar) def fmt(tuple_size): + """Format the result according to the tuple size.""" if isinstance(result, tuple): return result else: @@ -99,7 +99,7 @@ def _open_file(name, mode='w'): return open(name, mode, newline='\n', encoding='utf-8') -def _get_cmd_syntax(name, cmd): +def _get_cmd_syntax(_name, cmd): """Get the command syntax for a command. We monkey-patch the parser's formatter_class here to use our UsageFormatter @@ -369,7 +369,7 @@ def regenerate_authors(filename): def regenerate_manpage(filename): """Update manpage OPTIONS using an argparse parser.""" # pylint: disable=protected-access - parser = qutequtebrowser.get_argparser() + parser = qutebrowser.get_argparser() groups = [] # positionals, optionals and user-defined groups for group in parser._action_groups: @@ -388,6 +388,12 @@ def regenerate_manpage(filename): def call_asciidoc(src, dst): + """Call asciidoc for the given files. + + Args: + src: The source .asciidoc file. + dst: The destination .html file, or None to auto-guess. + """ print("{}Calling asciidoc for {}...{}".format( col.Fore.CYAN, os.path.basename(src), col.Fore.RESET)) args = ['asciidoc'] @@ -401,7 +407,8 @@ def call_asciidoc(src, dst): sys.exit(1) -if __name__ == '__main__': +def main(): + """Regenerate all documentation.""" print("{}Generating asciidoc files...{}".format( col.Fore.CYAN, col.Fore.RESET)) regenerate_manpage('doc/qutebrowser.1.asciidoc') @@ -416,3 +423,6 @@ if __name__ == '__main__': ('README.asciidoc', None)] for src, dst in asciidoc_files: call_asciidoc(src, dst) + +if __name__ == '__main__': + main() diff --git a/scripts/run_checks.py b/scripts/run_checks.py index 10f05a7f7..16aef3541 100755 --- a/scripts/run_checks.py +++ b/scripts/run_checks.py @@ -18,7 +18,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=broad-except, no-member +# pylint: disable=broad-except """ Run different codecheckers over a codebase. From 880758d04ecd9677b9cc10c2837c6bd7237b2ded Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Sep 2014 11:40:27 +0200 Subject: [PATCH 30/89] Make generate_doc work on Windows --- scripts/generate_doc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index ba7c7b35d..c6935ccc9 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -396,7 +396,11 @@ def call_asciidoc(src, dst): """ print("{}Calling asciidoc for {}...{}".format( col.Fore.CYAN, os.path.basename(src), col.Fore.RESET)) - args = ['asciidoc'] + if os.name == 'nt': + # FIXME this is highly specific to my machine + args = [r'C:\Python27\python', r'J:\bin\asciidoc-8.6.9\asciidoc.py'] + else: + args = ['asciidoc'] if dst is not None: args += ['--out-file', dst] args.append(src) From 070d5ae300a319a8b11f86a2a0282c84331b0bc5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Sep 2014 12:18:54 +0200 Subject: [PATCH 31/89] Add more documentation. --- doc/{ => help}/commands.asciidoc | 2 +- doc/help/index.asciidoc | 50 ++++++++++++++++++++++++++++++++ doc/{ => help}/settings.asciidoc | 0 qutebrowser/browser/commands.py | 6 ++-- scripts/generate_doc.py | 24 ++++++++++----- 5 files changed, 71 insertions(+), 11 deletions(-) rename doc/{ => help}/commands.asciidoc (99%) create mode 100644 doc/help/index.asciidoc rename doc/{ => help}/settings.asciidoc (100%) diff --git a/doc/commands.asciidoc b/doc/help/commands.asciidoc similarity index 99% rename from doc/commands.asciidoc rename to doc/help/commands.asciidoc index b533690ff..7ef9178d5 100644 --- a/doc/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -74,7 +74,7 @@ Get the value from a section/option. [[help]] === help -Syntax: +:help 'topic'+ +Syntax: +:help ['topic']+ Show help about a command or setting. diff --git a/doc/help/index.asciidoc b/doc/help/index.asciidoc new file mode 100644 index 000000000..b81ea1d5e --- /dev/null +++ b/doc/help/index.asciidoc @@ -0,0 +1,50 @@ +qutebrowser help +================ + +Documentation +------------- + +The following help pages are currently available: + +* link:FAQ.html[Frequently asked questions] +* link:commands.html[Documentation of commands] +* link:settings.html[Documentation of settings] + +Getting help +------------ + +You can get help in the IRC channel +irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on +http://freenode.net/[Freenode] +(https://webchat.freenode.net/?channels=#qutebrowser[webchat]), or by writing a +message to the +https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at +mailto:qutebrowser@lists.qutebrowser.org[]. + +Bugs +---- + +If you found a bug or have a feature request, you can report it in several +ways: + +* Use the built-in `:report` command or the automatic crash dialog. +* Open an issue in the Github issue tracker. +* Write a mail to the +https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at +mailto:qutebrowser@lists.qutebrowser.org[]. + +License +------- + +This program 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. + +This program 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 this program. If not, see . diff --git a/doc/settings.asciidoc b/doc/help/settings.asciidoc similarity index 100% rename from doc/settings.asciidoc rename to doc/help/settings.asciidoc diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 8bac8f0b0..2ee04a72c 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -669,7 +669,7 @@ class CommandDispatcher: @cmdutils.register(instance='mainwindow.tabs.cmd', name='help', completion=[usertypes.Completion.helptopic]) - def show_help(self, topic): + def show_help(self, topic=None): r"""Show help about a command or setting. Args: @@ -678,7 +678,9 @@ class CommandDispatcher: - :__command__ for commands. - __section__\->__option__ for settings. """ - if topic.startswith(':'): + if topic is None: + path = 'index.html' + elif topic.startswith(':'): command = topic[1:] if command not in cmdutils.cmd_dict: raise cmdexc.CommandError("Invalid command {}!".format( diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index c6935ccc9..0a104b303 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -22,6 +22,7 @@ import os import sys +import glob import html import shutil import os.path @@ -416,15 +417,22 @@ def main(): print("{}Generating asciidoc files...{}".format( col.Fore.CYAN, col.Fore.RESET)) regenerate_manpage('doc/qutebrowser.1.asciidoc') - generate_settings('doc/settings.asciidoc') - generate_commands('doc/commands.asciidoc') + generate_settings('doc/help/settings.asciidoc') + generate_commands('doc/help/commands.asciidoc') regenerate_authors('README.asciidoc') - asciidoc_files = [('doc/qutebrowser.1.asciidoc', None), - ('doc/settings.asciidoc', - 'qutebrowser/html/doc/settings.html'), - ('doc/commands.asciidoc', - 'qutebrowser/html/doc/commands.html'), - ('README.asciidoc', None)] + asciidoc_files = [ + ('doc/qutebrowser.1.asciidoc', None), + ('README.asciidoc', None), + ('doc/FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'), + ] + try: + os.mkdir('qutebrowser/html/doc') + except FileExistsError: + pass + for src in glob.glob('doc/help/*.asciidoc'): + name, _ext = os.path.splitext(os.path.basename(src)) + dst = 'qutebrowser/html/doc/{}.html'.format(name) + asciidoc_files.append((src, dst)) for src, dst in asciidoc_files: call_asciidoc(src, dst) From 64183b5a26fe3288976dfabe6a312563069efd45 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Sep 2014 16:53:33 +0200 Subject: [PATCH 32/89] Start moving keybindings --- qutebrowser/config/configdata.py | 650 ++++++++++++++++++++----------- 1 file changed, 425 insertions(+), 225 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 263c6cb8e..5fca0394e 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -80,61 +80,6 @@ SECTION_DESC = { "bang-syntax, e.g. `:open qutebrowser !google`. The string `{}` will " "be replaced by the search term, use `{{` and `}}` for literal " "`{`/`}` signs."), - 'keybind': ( - "Bindings from a key(chain) to a command.\n" - "For special keys (can't be part of a keychain), enclose them in " - "`<`...`>`. For modifiers, you can use either `-` or `+` as " - "delimiters, and these names:\n\n" - " * Control: `Control`, `Ctrl`\n" - " * Meta: `Meta`, `Windows`, `Mod4`\n" - " * Alt: `Alt`, `Mod1`\n" - " * Shift: `Shift`\n\n" - "For simple keys (no `<>`-signs), a capital letter means the key is " - "pressed with Shift. For special keys (with `<>`-signs), you need " - "to explicitely add `Shift-` to match a key pressed with shift. " - "You can bind multiple commands by separating them with `;;`."), - 'keybind.insert': ( - "Keybindings for insert mode.\n" - "Since normal keypresses are passed through, only special keys are " - "supported in this mode.\n" - "Useful hidden commands to map in this section:\n\n" - " * `open-editor`: Open a texteditor with the focused field.\n" - " * `leave-mode`: Leave the command mode."), - 'keybind.hint': ( - "Keybindings for hint mode.\n" - "Since normal keypresses are passed through, only special keys are " - "supported in this mode.\n" - "Useful hidden commands to map in this section:\n\n" - " * `follow-hint`: Follow the currently selected hint.\n" - " * `leave-mode`: Leave the command mode."), - 'keybind.passthrough': ( - "Keybindings for passthrough mode.\n" - "Since normal keypresses are passed through, only special keys are " - "supported in this mode.\n" - "Useful hidden commands to map in this section:\n\n" - " * `leave-mode`: Leave the passthrough mode."), - 'keybind.command': ( - "Keybindings for command mode.\n" - "Since normal keypresses are passed through, only special keys are " - "supported in this mode.\n" - "Useful hidden commands to map in this section:\n\n" - " * `command-history-prev`: Switch to previous command in history.\n" - " * `command-history-next`: Switch to next command in history.\n" - " * `completion-item-prev`: Select previous item in completion.\n" - " * `completion-item-next`: Select next item in completion.\n" - " * `command-accept`: Execute the command currently in the " - "commandline.\n" - " * `leave-mode`: Leave the command mode."), - 'keybind.prompt': ( - "Keybindings for prompts in the status line.\n" - "You can bind normal keys in this mode, but they will be only active " - "when a yes/no-prompt is asked. For other prompt modes, you can only " - "bind special keys.\n" - "Useful hidden commands to map in this section:\n\n" - " * `prompt-accept`: Confirm the entered value.\n" - " * `prompt-yes`: Answer yes to a yes/no question.\n" - " * `prompt-no`: Answer no to a yes/no question.\n" - " * `leave-mode`: Leave the prompt mode."), 'aliases': ( "Aliases for commands.\n" "By default, no aliases are defined. Example which adds a new command " @@ -581,176 +526,6 @@ DATA = collections.OrderedDict([ ('wiki', '${wikipedia}'), )), - ('keybind', sect.ValueList( - typ.KeyBindingName(), typ.KeyBinding(), - ('o', 'set-cmd-text ":open "'), - ('go', 'set-cmd-text ":open {url}"'), - ('O', 'set-cmd-text ":open -t "'), - ('gO', 'set-cmd-text ":open -t {url}"'), - ('xo', 'set-cmd-text ":open -b "'), - ('xO', 'set-cmd-text ":open -b {url}"'), - ('ga', 'open -t about:blank'), - ('d', 'tab-close'), - ('co', 'tab-only'), - ('T', 'tab-focus'), - ('gm', 'tab-move'), - ('gl', 'tab-move -'), - ('gr', 'tab-move +'), - ('J', 'tab-next'), - ('K', 'tab-prev'), - ('r', 'reload'), - ('H', 'back'), - ('L', 'forward'), - ('f', 'hint'), - ('F', 'hint all tab'), - (';b', 'hint all tab-bg'), - (';i', 'hint images'), - (';I', 'hint images tab'), - ('.i', 'hint images tab-bg'), - (';o', 'hint links fill ":open {hint-url}"'), - (';O', 'hint links fill ":open -t {hint-url}"'), - ('.o', 'hint links fill ":open -b {hint-url}"'), - (';y', 'hint links yank'), - (';Y', 'hint links yank-primary'), - (';r', 'hint links rapid'), - (';d', 'hint links download'), - ('h', 'scroll -50 0'), - ('j', 'scroll 0 50'), - ('k', 'scroll 0 -50'), - ('l', 'scroll 50 0'), - ('u', 'undo'), - ('gg', 'scroll-perc 0'), - ('G', 'scroll-perc'), - ('n', 'search-next'), - ('N', 'search-prev'), - ('i', 'enter-mode insert'), - ('yy', 'yank'), - ('yY', 'yank -s'), - ('yt', 'yank -t'), - ('yT', 'yank -ts'), - ('pp', 'paste'), - ('pP', 'paste -s'), - ('Pp', 'paste -t'), - ('PP', 'paste -ts'), - ('m', 'quickmark-save'), - ('b', 'set-cmd-text ":quickmark-load "'), - ('B', 'set-cmd-text ":quickmark-load -t "'), - ('sf', 'save'), - ('ss', 'set-cmd-text ":set "'), - ('sl', 'set-cmd-text ":set -t "'), - ('sk', 'set-cmd-text ":set keybind "'), - ('-', 'zoom-out'), - ('+', 'zoom-in'), - ('=', 'zoom'), - ('[[', 'prev-page'), - (']]', 'next-page'), - ('{{', 'prev-page -t'), - ('}}', 'next-page -t'), - ('wi', 'inspector'), - ('gd', 'download-page'), - ('ad', 'cancel-download'), - ('', 'tab-focus last'), - ('', 'enter-mode passthrough'), - ('', 'quit'), - ('', 'undo'), - ('', 'tab-close'), - ('', 'open -t about:blank'), - ('', 'scroll-page 0 1'), - ('', 'scroll-page 0 -1'), - ('', 'scroll-page 0 0.5'), - ('', 'scroll-page 0 -0.5'), - ('', 'tab-focus 1'), - ('', 'tab-focus 2'), - ('', 'tab-focus 3'), - ('', 'tab-focus 4'), - ('', 'tab-focus 5'), - ('', 'tab-focus 6'), - ('', 'tab-focus 7'), - ('', 'tab-focus 8'), - ('', 'tab-focus 9'), - ('', 'back'), - ('', 'home'), - ('', 'stop'), - ('', 'print'), - )), - - ('keybind.insert', sect.ValueList( - typ.KeyBindingName(), typ.KeyBinding(), - ('', 'leave-mode'), - ('', 'leave-mode'), - ('', 'open-editor'), - ('', '${}'), - )), - - ('keybind.hint', sect.ValueList( - typ.KeyBindingName(), typ.KeyBinding(), - ('', 'follow-hint'), - ('', 'leave-mode'), - ('', 'leave-mode'), - ('', '${}'), - )), - - ('keybind.passthrough', sect.ValueList( - typ.KeyBindingName(), typ.KeyBinding(), - ('', 'leave-mode'), - ('', '${}'), - )), - - # FIXME we should probably have a common section for input modes with a - # text field. - - ('keybind.command', sect.ValueList( - typ.KeyBindingName(), typ.KeyBinding(), - ('', 'leave-mode'), - ('', 'command-history-prev'), - ('', 'command-history-next'), - ('', 'completion-item-prev'), - ('', 'completion-item-prev'), - ('', 'completion-item-next'), - ('', 'completion-item-next'), - ('', 'command-accept'), - ('', 'command-accept'), - ('', 'rl-backward-char'), - ('', 'rl-forward-char'), - ('', 'rl-backward-word'), - ('', 'rl-forward-word'), - ('', 'rl-beginning-of-line'), - ('', 'rl-end-of-line'), - ('', 'rl-unix-line-discard'), - ('', 'rl-kill-line'), - ('', 'rl-kill-word'), - ('', 'rl-unix-word-rubout'), - ('', 'rl-yank'), - ('', 'rl-delete-char'), - ('', 'rl-backward-delete-char'), - ('', '${}'), - ('', '${}'), - )), - - ('keybind.prompt', sect.ValueList( - typ.KeyBindingName(), typ.KeyBinding(), - ('', 'leave-mode'), - ('', 'prompt-accept'), - ('', 'prompt-accept'), - ('y', 'prompt-yes'), - ('n', 'prompt-no'), - ('', 'rl-backward-char'), - ('', 'rl-forward-char'), - ('', 'rl-backward-word'), - ('', 'rl-forward-word'), - ('', 'rl-beginning-of-line'), - ('', 'rl-end-of-line'), - ('', 'rl-unix-line-discard'), - ('', 'rl-kill-line'), - ('', 'rl-kill-word'), - ('', 'rl-unix-word-rubout'), - ('', 'rl-yank'), - ('', 'rl-delete-char'), - ('', 'rl-backward-delete-char'), - ('', '${}'), - ('', '${}'), - )), - ('aliases', sect.ValueList( typ.String(forbidden=' '), typ.Command(), )), @@ -1004,3 +779,428 @@ DATA = collections.OrderedDict([ "The default font size for fixed-pitch text."), )), ]) + +KEYBINDINGS = """ +# Bindings from a key(chain) to a command. +# +# For special keys (can't be part of a keychain), enclose them in `<`...`>`. +# For modifiers, you can use either `-` or `+` as delimiters, and these names: +# +# * Control: `Control`, `Ctrl` +# * Meta: `Meta`, `Windows`, `Mod4` +# * Alt: `Alt`, `Mod1` +# * Shift: `Shift` +# +# For simple keys (no `<>`-signs), a capital letter means the key is pressed +# with Shift. For special keys (with `<>`-signs), you need to explicitely add +# `Shift-` to match a key pressed with shift. You can bind multiple commands +# by separating them with `;;`. + +[normal] + +set-cmd-text ":open " + o + +set-cmd-text ":open {url}" + go + +set-cmd-text ":open -t " + O + +set-cmd-text ":open -t {url}" + gO + +set-cmd-text ":open -b " + xo + +set-cmd-text ":open -b {url}" + xO + +open -t about:blank + ga + +tab-close + d + +tab-only + co + +tab-focus + T + +tab-move + gm + +tab-move - + gl + +tab-move + + gr + +tab-next + J + +tab-prev + K + +reload + r + +back + H + +forward + L + +hint + f + +hint all tab + F + +hint all tab-bg + ;b + +hint images + ;i + +hint images tab + ;I + +hint images tab-bg + .i + +hint links fill ":open {hint-url}" + ;o + +hint links fill ":open -t {hint-url}" + ;O + +hint links fill ":open -b {hint-url}" + .o + +hint links yank + ;y + +hint links yank-primary + ;Y + +hint links rapid + ;r + +hint links download + ;d + +scroll -50 0 + h + +scroll 0 50 + j + +scroll 0 -50 + k + +scroll 50 0 + l + +undo + u + +scroll-perc 0 + gg + +scroll-perc + G + +search-next + n + +search-prev + N + +enter-mode insert + i + +yank + yy + +yank -s + yY + +yank -t + yt + +yank -ts + yT + +paste + pp + +paste -s + pP + +paste -t + Pp + +paste -ts + PP + +quickmark-save + m + +set-cmd-text ":quickmark-load " + b + +set-cmd-text ":quickmark-load -t " + B + +save + sf + +set-cmd-text ":set " + ss + +set-cmd-text ":set -t " + sl + +set-cmd-text ":set keybind " + sk + +zoom-out + - + +zoom-in + + + +zoom + = + +prev-page + [[ + +next-page + ]] + +prev-page -t + {{ + +next-page -t + }} + +inspector + wi + +download-page + gd + +cancel-download + ad + +tab-focus last + + +enter-mode passthrough + + +quit + + +undo + + +tab-close + + +open -t about:blank + + +scroll-page 0 1 + + +scroll-page 0 -1 + + +scroll-page 0 0.5 + + +scroll-page 0 -0.5 + + +tab-focus 1 + + +tab-focus 2 + + +tab-focus 3 + + +tab-focus 4 + + +tab-focus 5 + + +tab-focus 6 + + +tab-focus 7 + + +tab-focus 8 + + +tab-focus 9 + + +back + + +home + + +stop + + +print + + +[insert,hint,passthrough,command,prompt] + +leave-mode + + + + +[passthrough] +# Keybindings for passthrough mode. +# +# Since normal keypresses are passed through, only special keys are supported +# in this section. + +[insert] + +# Since normal keypresses are passed through, only special keys are supported +# in this section. +# +# Useful hidden commands to map in this section: +# * `open-editor`: Open a texteditor with the focused field. + +open-editor + + +[hint] + +# Since normal keypresses are passed through, only special keys are supported +# in this section. +# +# Useful hidden commands to map in this section: +# +# * `follow-hint`: Follow the currently selected hint. + +follow-hint + + +[command,prompt] + +rl-backward-char + + +rl-forward-char + + +rl-backward-word + + +rl-forward-word + + +rl-beginning-of-line + + +rl-end-of-line + + +rl-unix-line-discard + + +rl-kill-line + + +rl-kill-word + + +rl-unix-word-rubout + + +rl-yank + + +rl-delete-char + + +rl-backward-delete-char + + + +[command] + +# Since normal keypresses are passed through, only special keys are +# supported in this mode. + +# Useful hidden commands to map in this section: +# +# * `command-history-prev`: Switch to previous command in history. +# * `command-history-next`: Switch to next command in history. +# * `completion-item-prev`: Select previous item in completion. +# * `completion-item-next`: Select next item in completion. +# * `command-accept`: Execute the command currently in the commandline. +# * `leave-mode`: Leave the command mode. + +command-history-prev + + +command-history-next + + +completion-item-prev + + +completion-item-prev + + +completion-item-next + + +completion-item-next + + +command-accept + + + + +[prompt] + +# You can bind normal keys in this mode, but they will be only active when a +# yes/no-prompt is asked. For other prompt modes, you can only bind special +# keys. + +# Useful hidden commands to map in this section: +# +# * `prompt-accept`: Confirm the entered value. +# * `prompt-yes`: Answer yes to a yes/no question. +# * `prompt-no`: Answer no to a yes/no question. +# * `leave-mode`: Leave the prompt mode. + +prompt-accept + + + + +prompt-yes + y + +prompt-no + n +""" From 6f03f081112f3a0365783915002cd7dac57ee0bd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Sep 2014 07:43:27 +0200 Subject: [PATCH 33/89] Make new key config work (readonly) --- qutebrowser/app.py | 24 ++++++++++++++----- qutebrowser/keyinput/basekeyparser.py | 34 +++++++++++++-------------- qutebrowser/keyinput/keyparser.py | 14 +++++------ qutebrowser/keyinput/modeparsers.py | 8 +++---- 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 0b7dd2d13..ce57d763d 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -37,7 +37,7 @@ from PyQt5.QtCore import (pyqtSlot, QTimer, QEventLoop, Qt, QStandardPaths, import qutebrowser from qutebrowser.commands import userscripts, runners, cmdutils from qutebrowser.config import (style, config, websettings, iniparsers, - lineparser, configtypes) + lineparser, configtypes, keyconfparser) from qutebrowser.network import qutescheme, proxy from qutebrowser.browser import quickmarks, cookies, downloads, cache from qutebrowser.widgets import mainwindow, console, crash @@ -190,6 +190,19 @@ class Application(QApplication): msgbox.exec_() # We didn't really initialize much so far, so we just quit hard. sys.exit(1) + try: + self.keyconfig = keyconfparser.KeyConfigParser(confdir, 'keys') + except keyconfparser.KeyConfigError as e: + log.init.exception(e) + errstr = "Error while reading key config:\n" + if hasattr(e, 'lineno'): + errstr += "In line {}: ".format(e.lineno) + errstr += str(e) + msgbox = QMessageBox(QMessageBox.Critical, + "Error while reading key config!", errstr) + msgbox.exec_() + # We didn't really initialize much so far, so we just quit hard. + sys.exit(1) self.stateconfig = iniparsers.ReadWriteConfigParser(confdir, 'state') self.cmd_history = lineparser.LineConfigParser( confdir, 'cmd_history', ('completion', 'history-length')) @@ -202,14 +215,13 @@ class Application(QApplication): utypes.KeyMode.hint: modeparsers.HintKeyParser(self), utypes.KeyMode.insert: - keyparser.PassthroughKeyParser('keybind.insert', self), + keyparser.PassthroughKeyParser('insert', self), utypes.KeyMode.passthrough: - keyparser.PassthroughKeyParser('keybind.passthrough', self), + keyparser.PassthroughKeyParser('passthrough', self), utypes.KeyMode.command: - keyparser.PassthroughKeyParser('keybind.command', self), + keyparser.PassthroughKeyParser('command', self), utypes.KeyMode.prompt: - keyparser.PassthroughKeyParser('keybind.prompt', self, - warn=False), + keyparser.PassthroughKeyParser('prompt', self, warn=False), utypes.KeyMode.yesno: modeparsers.PromptKeyParser(self), } diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 800428d50..bf34f5c57 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -23,7 +23,7 @@ import re import string import functools -from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject, QCoreApplication from qutebrowser.config import config from qutebrowser.utils import usertypes, log, utils @@ -57,7 +57,7 @@ class BaseKeyParser(QObject): keychains in a section which does not support them. _keystring: The currently entered key sequence _timer: Timer for delayed execution. - _confsectname: The name of the configsection. + _modename: The name of the input mode associated with this keyparser. _supports_count: Whether count is supported _supports_chains: Whether keychains are supported @@ -77,7 +77,7 @@ class BaseKeyParser(QObject): supports_chains=False): super().__init__(parent) self._timer = None - self._confsectname = None + self._modename = None self._keystring = '' if supports_count is None: supports_count = supports_chains @@ -303,28 +303,26 @@ class BaseKeyParser(QObject): self.keystring_updated.emit(self._keystring) return handled - def read_config(self, sectname=None): + def read_config(self, modename=None): """Read the configuration. Config format: key = command, e.g.: = quit Args: - sectname: Name of the section to read. + modename: Name of the mode to use. """ - if sectname is None: - if self._confsectname is None: - raise ValueError("read_config called with no section, but " + if modename is None: + if self._modename is None: + raise ValueError("read_config called with no mode given, but " "None defined so far!") - sectname = self._confsectname + modename = self._modename else: - self._confsectname = sectname + self._modename = modename self.bindings = {} self.special_bindings = {} - sect = config.section(sectname) - if not sect.items(): - log.keyboard.warning("No keybindings defined!") - for (key, cmd) in sect.items(): + keyconfparser = QCoreApplication.instance().keyconfig + for (key, cmd) in keyconfparser.get_bindings_for(modename).items(): if not cmd: continue elif key.startswith('<') and key.endswith('>'): @@ -334,8 +332,8 @@ class BaseKeyParser(QObject): self.bindings[key] = cmd elif self.warn_on_keychains: log.keyboard.warning( - "Ignoring keychain '{}' in section '{}' because " - "keychains are not supported there.".format(key, sectname)) + "Ignoring keychain '{}' in mode '{}' because " + "keychains are not supported there.".format(key, modename)) def execute(self, cmdstr, keytype, count=None): """Handle a completed keychain. @@ -350,8 +348,8 @@ class BaseKeyParser(QObject): @pyqtSlot(str, str) def on_config_changed(self, section, _option): """Re-read the config if a keybinding was changed.""" - if self._confsectname is None: + if self._modename is None: raise AttributeError("on_config_changed called but no section " "defined!") - if section == self._confsectname: + if section == self._modename: self.read_config() diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index bc1946a52..8529f459c 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -51,25 +51,25 @@ class PassthroughKeyParser(CommandKeyParser): Used for insert/passthrough modes. Attributes: - _confsect: The config section to use. + _mode: The mode this keyparser is for. """ do_log = False - def __init__(self, confsect, parent=None, warn=True): + def __init__(self, mode, parent=None, warn=True): """Constructor. Args: - confsect: The config section to use. + mode: The mode this keyparser is for. parent: Qt parent. warn: Whether to warn if an ignored key was bound. """ super().__init__(parent, supports_chains=False) self.log = False self.warn_on_keychains = warn - self.read_config(confsect) - self._confsect = confsect + self.read_config(mode) + self._mode = mode def __repr__(self): - return '<{} confsect={}, warn={})'.format( - self.__class__.__name__, self._confsect, self.warn_on_keychains) + return '<{} mode={}, warn={})'.format( + self.__class__.__name__, self._mode, self.warn_on_keychains) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index eccaef117..e22bcd801 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -41,7 +41,7 @@ class NormalKeyParser(keyparser.CommandKeyParser): def __init__(self, parent=None): super().__init__(parent, supports_count=True, supports_chains=True) - self.read_config('keybind') + self.read_config('normal') def __repr__(self): return '<{}>'.format(self.__class__.__name__) @@ -69,8 +69,8 @@ class PromptKeyParser(keyparser.CommandKeyParser): def __init__(self, parent=None): super().__init__(parent, supports_count=False, supports_chains=True) # We don't want an extra section for this in the config, so we just - # abuse the keybind.prompt section. - self.read_config('keybind.prompt') + # abuse the prompt section. + self.read_config('prompt') def __repr__(self): return '<{}>'.format(self.__class__.__name__) @@ -98,7 +98,7 @@ class HintKeyParser(keyparser.CommandKeyParser): super().__init__(parent, supports_count=False, supports_chains=True) self._filtertext = '' self._last_press = LastPress.none - self.read_config('keybind.hint') + self.read_config('hint') def _handle_special_key(self, e): """Override _handle_special_key to handle string filtering. From 91514ad6c0ae86c5e881ff133b13761f6d25d872 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Sep 2014 13:42:25 +0200 Subject: [PATCH 34/89] Add missing keyconfparser file. --- qutebrowser/config/keyconfparser.py | 108 ++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 qutebrowser/config/keyconfparser.py diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py new file mode 100644 index 000000000..a2e9ec4ce --- /dev/null +++ b/qutebrowser/config/keyconfparser.py @@ -0,0 +1,108 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014 Florian Bruhin (The Compiler) +# +# 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 . + +"""Parser for the key configuration.""" + + +import os.path + +from qutebrowser.config import configdata +from qutebrowser.commands import cmdutils +from qutebrowser.utils import log + + +class KeyConfigError(Exception): + + """Raised on errors with the key config.""" + + +class KeyConfigParser: + + """Parser for the keybind config.""" + + def __init__(self, configdir, fname): + """Constructor. + + Args: + configdir: The directory to save the configs in. + fname: The filename of the config. + """ + self._configdir = configdir + self._configfile = os.path.join(self._configdir, fname) + self._cur_section = None + self._cur_command = None + # Mapping of section name(s) to keybinding -> command dicts. + self.keybindings = {} + if not os.path.exists(self._configfile): + log.init.debug("Creating initial keybinding config.") + with open(self._configfile, 'w', encoding='utf-8') as f: + f.write(configdata.KEYBINDINGS) + self._read() + + def _read(self): + """Read the config file from disk and parse it.""" + with open(self._configfile, 'r', encoding='utf-8') as f: + for i, line in enumerate(f): + line = line.rstrip() + try: + if not line.strip() or line.startswith('#'): + continue + elif line.startswith('[') and line.endswith(']'): + self._cur_section = line[1:-1] + elif line.startswith((' ', '\t')): + line = line.strip() + self._read_keybinding(line) + else: + line = line.strip() + self._read_command(line) + except KeyConfigError as e: + e.lineno = i + raise + + def _read_command(self, line): + """Read a command from a line.""" + if self._cur_section is None: + raise KeyConfigError("Got command '{}' without getting a " + "section!".format(line)) + else: + command = line.split(maxsplit=1)[0] + if command not in cmdutils.cmd_dict: + raise KeyConfigError("Invalid command '{}'!".format(command)) + self._cur_command = line + + def _read_keybinding(self, line): + """Read a keybinding from a line.""" + if self._cur_command is None: + raise KeyConfigError("Got keybinding '{}' without getting a " + "command!".format(line)) + else: + assert self._cur_section is not None + if self._cur_section not in self.keybindings: + self.keybindings[self._cur_section] = {} + section_bindings = self.keybindings[self._cur_section] + section_bindings[line] = self._cur_command + + def get_bindings_for(self, section): + """Get a dict with all merged keybindings for a section.""" + bindings = {} + for sectstring, d in self.keybindings.items(): + sects = [s.strip() for s in sectstring.split(',')] + if any(s == section for s in sects): + bindings.update(d) + return bindings From 414ab88a0ee3ab5dfe6bff1096e6029d965a381e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Sep 2014 18:32:18 +0200 Subject: [PATCH 35/89] Fix lint --- qutebrowser/app.py | 3 ++- qutebrowser/config/keyconfparser.py | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index ce57d763d..2f7fedb82 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -103,6 +103,7 @@ class Application(QApplication): self.modeman = None self.cmd_history = None self.config = None + self.keyconfig = None sys.excepthook = self._exception_hook @@ -195,7 +196,7 @@ class Application(QApplication): except keyconfparser.KeyConfigError as e: log.init.exception(e) errstr = "Error while reading key config:\n" - if hasattr(e, 'lineno'): + if e.lineno is not None: errstr += "In line {}: ".format(e.lineno) errstr += str(e) msgbox = QMessageBox(QMessageBox.Critical, diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py index a2e9ec4ce..9e8cd2dd9 100644 --- a/qutebrowser/config/keyconfparser.py +++ b/qutebrowser/config/keyconfparser.py @@ -29,7 +29,15 @@ from qutebrowser.utils import log class KeyConfigError(Exception): - """Raised on errors with the key config.""" + """Raised on errors with the key config. + + Attributes: + lineno: The config line in which the exception occured. + """ + + def __init__(self): + super().__init__() + self.lineno = None class KeyConfigParser: From e3d16f3bbee60a384eae581c612d2867f8d2c8f4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Sep 2014 21:40:16 +0200 Subject: [PATCH 36/89] Full read-write support for key config. --- qutebrowser/app.py | 10 +- qutebrowser/config/config.py | 19 +- qutebrowser/config/configdata.py | 596 ++++++------------ qutebrowser/config/configtypes.py | 19 - qutebrowser/config/keyconfparser.py | 87 ++- qutebrowser/config/textwrapper.py | 39 ++ qutebrowser/test/config/test_configtypes.py | 26 - .../test/keyinput/test_basekeyparser.py | 33 +- qutebrowser/test/stubs.py | 3 +- qutebrowser/test/utils/test_readline.py | 5 +- 10 files changed, 338 insertions(+), 499 deletions(-) create mode 100644 qutebrowser/config/textwrapper.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 2f7fedb82..194ce919b 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -192,7 +192,8 @@ class Application(QApplication): # We didn't really initialize much so far, so we just quit hard. sys.exit(1) try: - self.keyconfig = keyconfparser.KeyConfigParser(confdir, 'keys') + self.keyconfig = keyconfparser.KeyConfigParser( + confdir, 'keys.conf') except keyconfparser.KeyConfigError as e: log.init.exception(e) errstr = "Error while reading key config:\n" @@ -730,7 +731,7 @@ class Application(QApplication): # event loop, so we can shut down immediately. self._shutdown(status) - def _shutdown(self, status): + def _shutdown(self, status): # noqa """Second stage of shutdown.""" log.destroy.debug("Stage 2 of shutting down...") # Remove eventfilter @@ -745,7 +746,10 @@ class Application(QApplication): if hasattr(self, 'config') and self.config is not None: to_save = [] if self.config.get('general', 'auto-save-config'): - to_save.append(("config", self.config.save)) + if hasattr(self, 'config'): + to_save.append(("config", self.config.save)) + if hasattr(self, 'keyconfig'): + to_save.append(("keyconfig", self.keyconfig.save)) to_save += [("window geometry", self._save_geometry), ("quickmarks", quickmarks.save)] if hasattr(self, 'cmd_history'): diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 3d0f6516e..267177617 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -26,7 +26,6 @@ we borrow some methods and classes from there where it makes sense. import os import os.path -import textwrap import functools import configparser import collections.abc @@ -34,7 +33,7 @@ import collections.abc from PyQt5.QtCore import pyqtSignal, QObject, QCoreApplication from qutebrowser.utils import log -from qutebrowser.config import configdata, iniparsers, configtypes +from qutebrowser.config import configdata, iniparsers, configtypes, textwrapper from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.utils import message from qutebrowser.utils.usertypes import Completion @@ -89,7 +88,6 @@ class ConfigManager(QObject): sections: The configuration data as an OrderedDict. _fname: The filename to be opened. _configparser: A ReadConfigParser instance to load the config. - _wrapper_args: A dict with the default kwargs for the config wrappers. _configdir: The dictionary to read the config from and save it in. _configfile: The config file path. _interpolation: An configparser.Interpolation object @@ -113,12 +111,6 @@ class ConfigManager(QObject): self.sections = configdata.DATA self._configparser = iniparsers.ReadConfigParser(configdir, fname) self._configfile = os.path.join(configdir, fname) - self._wrapper_args = { - 'width': 72, - 'replace_whitespace': False, - 'break_long_words': False, - 'break_on_hyphens': False, - } self._configdir = configdir self._fname = fname self._interpolation = configparser.ExtendedInterpolation() @@ -146,9 +138,7 @@ class ConfigManager(QObject): def _str_section_desc(self, sectname): """Get the section description string for sectname.""" - wrapper = textwrap.TextWrapper(initial_indent='# ', - subsequent_indent='# ', - **self._wrapper_args) + wrapper = textwrapper.TextWrapper() lines = [] seclines = configdata.SECTION_DESC[sectname].splitlines() for secline in seclines: @@ -160,9 +150,8 @@ class ConfigManager(QObject): def _str_option_desc(self, sectname, sect): """Get the option description strings for sect/sectname.""" - wrapper = textwrap.TextWrapper(initial_indent='#' + ' ' * 5, - subsequent_indent='#' + ' ' * 5, - **self._wrapper_args) + wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5, + subsequent_indent='#' + ' ' * 5) lines = [] if not getattr(sect, 'descriptions', None): return lines diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 5fca0394e..55e03da9f 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -780,7 +780,10 @@ DATA = collections.OrderedDict([ )), ]) -KEYBINDINGS = """ + +KEY_FIRST_COMMENT = """ +# vim: ft=conf +# # Bindings from a key(chain) to a command. # # For special keys (can't be part of a keychain), enclose them in `<`...`>`. @@ -795,412 +798,187 @@ KEYBINDINGS = """ # with Shift. For special keys (with `<>`-signs), you need to explicitely add # `Shift-` to match a key pressed with shift. You can bind multiple commands # by separating them with `;;`. - -[normal] - -set-cmd-text ":open " - o - -set-cmd-text ":open {url}" - go - -set-cmd-text ":open -t " - O - -set-cmd-text ":open -t {url}" - gO - -set-cmd-text ":open -b " - xo - -set-cmd-text ":open -b {url}" - xO - -open -t about:blank - ga - -tab-close - d - -tab-only - co - -tab-focus - T - -tab-move - gm - -tab-move - - gl - -tab-move + - gr - -tab-next - J - -tab-prev - K - -reload - r - -back - H - -forward - L - -hint - f - -hint all tab - F - -hint all tab-bg - ;b - -hint images - ;i - -hint images tab - ;I - -hint images tab-bg - .i - -hint links fill ":open {hint-url}" - ;o - -hint links fill ":open -t {hint-url}" - ;O - -hint links fill ":open -b {hint-url}" - .o - -hint links yank - ;y - -hint links yank-primary - ;Y - -hint links rapid - ;r - -hint links download - ;d - -scroll -50 0 - h - -scroll 0 50 - j - -scroll 0 -50 - k - -scroll 50 0 - l - -undo - u - -scroll-perc 0 - gg - -scroll-perc - G - -search-next - n - -search-prev - N - -enter-mode insert - i - -yank - yy - -yank -s - yY - -yank -t - yt - -yank -ts - yT - -paste - pp - -paste -s - pP - -paste -t - Pp - -paste -ts - PP - -quickmark-save - m - -set-cmd-text ":quickmark-load " - b - -set-cmd-text ":quickmark-load -t " - B - -save - sf - -set-cmd-text ":set " - ss - -set-cmd-text ":set -t " - sl - -set-cmd-text ":set keybind " - sk - -zoom-out - - - -zoom-in - + - -zoom - = - -prev-page - [[ - -next-page - ]] - -prev-page -t - {{ - -next-page -t - }} - -inspector - wi - -download-page - gd - -cancel-download - ad - -tab-focus last - - -enter-mode passthrough - - -quit - - -undo - - -tab-close - - -open -t about:blank - - -scroll-page 0 1 - - -scroll-page 0 -1 - - -scroll-page 0 0.5 - - -scroll-page 0 -0.5 - - -tab-focus 1 - - -tab-focus 2 - - -tab-focus 3 - - -tab-focus 4 - - -tab-focus 5 - - -tab-focus 6 - - -tab-focus 7 - - -tab-focus 8 - - -tab-focus 9 - - -back - - -home - - -stop - - -print - - -[insert,hint,passthrough,command,prompt] - -leave-mode - - - - -[passthrough] -# Keybindings for passthrough mode. -# -# Since normal keypresses are passed through, only special keys are supported -# in this section. - -[insert] - -# Since normal keypresses are passed through, only special keys are supported -# in this section. -# -# Useful hidden commands to map in this section: -# * `open-editor`: Open a texteditor with the focused field. - -open-editor - - -[hint] - -# Since normal keypresses are passed through, only special keys are supported -# in this section. -# -# Useful hidden commands to map in this section: -# -# * `follow-hint`: Follow the currently selected hint. - -follow-hint - - -[command,prompt] - -rl-backward-char - - -rl-forward-char - - -rl-backward-word - - -rl-forward-word - - -rl-beginning-of-line - - -rl-end-of-line - - -rl-unix-line-discard - - -rl-kill-line - - -rl-kill-word - - -rl-unix-word-rubout - - -rl-yank - - -rl-delete-char - - -rl-backward-delete-char - - - -[command] - -# Since normal keypresses are passed through, only special keys are -# supported in this mode. - -# Useful hidden commands to map in this section: -# -# * `command-history-prev`: Switch to previous command in history. -# * `command-history-next`: Switch to next command in history. -# * `completion-item-prev`: Select previous item in completion. -# * `completion-item-next`: Select next item in completion. -# * `command-accept`: Execute the command currently in the commandline. -# * `leave-mode`: Leave the command mode. - -command-history-prev - - -command-history-next - - -completion-item-prev - - -completion-item-prev - - -completion-item-next - - -completion-item-next - - -command-accept - - - - -[prompt] - -# You can bind normal keys in this mode, but they will be only active when a -# yes/no-prompt is asked. For other prompt modes, you can only bind special -# keys. - -# Useful hidden commands to map in this section: -# -# * `prompt-accept`: Confirm the entered value. -# * `prompt-yes`: Answer yes to a yes/no question. -# * `prompt-no`: Answer no to a yes/no question. -# * `leave-mode`: Leave the prompt mode. - -prompt-accept - - - - -prompt-yes - y - -prompt-no - n """ + +KEY_SECTION_DESC = { + 'all': "Keybindings active in all modes.", + 'normal': "Keybindings for normal mode.", + 'insert': ( + "Keybindings for insert mode.\n" + "Since normal keypresses are passed through, only special keys are " + "supported in this mode.\n" + "Useful hidden commands to map in this section:\n\n" + " * `open-editor`: Open a texteditor with the focused field."), + 'hint': ( + "Keybindings for hint mode.\n" + "Since normal keypresses are passed through, only special keys are " + "supported in this mode.\n" + "Useful hidden commands to map in this section:\n\n" + " * `follow-hint`: Follow the currently selected hint."), + 'passthrough': ( + "Keybindings for passthrough mode.\n" + "Since normal keypresses are passed through, only special keys are " + "supported in this mode."), + 'command': ( + "Keybindings for command mode.\n" + "Since normal keypresses are passed through, only special keys are " + "supported in this mode.\n" + "Useful hidden commands to map in this section:\n\n" + " * `command-history-prev`: Switch to previous command in history.\n" + " * `command-history-next`: Switch to next command in history.\n" + " * `completion-item-prev`: Select previous item in completion.\n" + " * `completion-item-next`: Select next item in completion.\n" + " * `command-accept`: Execute the command currently in the " + "commandline."), + 'prompt': ( + "Keybindings for prompts in the status line.\n" + "You can bind normal keys in this mode, but they will be only active " + "when a yes/no-prompt is asked. For other prompt modes, you can only " + "bind special keys.\n" + "Useful hidden commands to map in this section:\n\n" + " * `prompt-accept`: Confirm the entered value.\n" + " * `prompt-yes`: Answer yes to a yes/no question.\n" + " * `prompt-no`: Answer no to a yes/no question."), +} + + +KEY_DATA = collections.OrderedDict([ + ('all', collections.OrderedDict([ + ('leave-mode', ['', '']), + ])), + + ('normal', collections.OrderedDict([ + ('set-cmd-text ":open "', ['o']), + ('set-cmd-text ":open {url}"', ['go']), + ('set-cmd-text ":open -t "', ['O']), + ('set-cmd-text ":open -t {url}"', ['gO']), + ('set-cmd-text ":open -b "', ['xo']), + ('set-cmd-text ":open -b {url}"', ['xO']), + ('open -t about:blank', ['ga']), + ('tab-close', ['d']), + ('tab-only', ['co']), + ('tab-focus', ['T']), + ('tab-move', ['gm']), + ('tab-move -', ['gl']), + ('tab-move +', ['gr']), + ('tab-next', ['J']), + ('tab-prev', ['K']), + ('reload', ['r']), + ('back', ['H']), + ('forward', ['L']), + ('hint', ['f']), + ('hint all tab', ['F']), + ('hint all tab-bg', [';b']), + ('hint images', [';i']), + ('hint images tab', [';I']), + ('hint images tab-bg', ['.i']), + ('hint links fill ":open {hint-url}"', [';o']), + ('hint links fill ":open -t {hint-url}"', [';O']), + ('hint links fill ":open -b {hint-url}"', ['.o']), + ('hint links yank', [';y']), + ('hint links yank-primary', [';Y']), + ('hint links rapid', [';r']), + ('hint links download', [';d']), + ('scroll -50 0', ['h']), + ('scroll 0 50', ['j']), + ('scroll 0 -50', ['k']), + ('scroll 50 0', ['l']), + ('undo', ['u']), + ('scroll-perc 0', ['gg']), + ('scroll-perc', ['G']), + ('search-next', ['n']), + ('search-prev', ['N']), + ('enter-mode insert', ['i']), + ('yank', ['yy']), + ('yank -s', ['yY']), + ('yank -t', ['yt']), + ('yank -ts', ['yT']), + ('paste', ['pp']), + ('paste -s', ['pP']), + ('paste -t', ['Pp']), + ('paste -ts', ['PP']), + ('quickmark-save', ['m']), + ('set-cmd-text ":quickmark-load "', ['b']), + ('set-cmd-text ":quickmark-load -t "', ['B']), + ('save', ['sf']), + ('set-cmd-text ":set "', ['ss']), + ('set-cmd-text ":set -t "', ['sl']), + ('set-cmd-text ":set keybind "', ['sk']), + ('zoom-out', ['-']), + ('zoom-in', ['+']), + ('zoom', ['=']), + ('prev-page', ['[[']), + ('next-page', [']]']), + ('prev-page -t', ['{{']), + ('next-page -t', ['}}']), + ('inspector', ['wi']), + ('download-page', ['gd']), + ('cancel-download', ['ad']), + ('tab-focus last', ['']), + ('enter-mode passthrough', ['']), + ('quit', ['']), + ('undo', ['']), + ('tab-close', ['']), + ('open -t about:blank', ['']), + ('scroll-page 0 1', ['']), + ('scroll-page 0 -1', ['']), + ('scroll-page 0 0.5', ['']), + ('scroll-page 0 -0.5', ['']), + ('tab-focus 1', ['']), + ('tab-focus 2', ['']), + ('tab-focus 3', ['']), + ('tab-focus 4', ['']), + ('tab-focus 5', ['']), + ('tab-focus 6', ['']), + ('tab-focus 7', ['']), + ('tab-focus 8', ['']), + ('tab-focus 9', ['']), + ('back', ['']), + ('home', ['']), + ('stop', ['']), + ('print', ['']), + ])), + + ('insert', collections.OrderedDict([ + ('open-editor', ['']), + ])), + + ('hint', collections.OrderedDict([ + ('follow-hint', ['']), + ])), + + ('passthrough', {}), + + ('command', collections.OrderedDict([ + ('command-history-prev', ['']), + ('command-history-next', ['']), + ('completion-item-prev', ['']), + ('completion-item-prev', ['']), + ('completion-item-next', ['']), + ('completion-item-next', ['']), + ('command-accept', ['', '', '']), + ])), + + ('prompt', collections.OrderedDict([ + ('prompt-accept', ['', '']), + ('prompt-accept', ['']), + ('prompt-yes', ['y']), + ('prompt-no', ['n']), + ])), + + ('command,prompt', collections.OrderedDict([ + ('rl-backward-char', ['']), + ('rl-forward-char', ['']), + ('rl-backward-word', ['']), + ('rl-forward-word', ['']), + ('rl-beginning-of-line', ['']), + ('rl-end-of-line', ['']), + ('rl-unix-line-discard', ['']), + ('rl-kill-line', ['']), + ('rl-kill-word', ['']), + ('rl-unix-word-rubout', ['']), + ('rl-yank', ['']), + ('rl-delete-char', ['']), + ('rl-backward-delete-char', ['']), + ])), +]) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index fada5ae1b..15f6dd062 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1042,25 +1042,6 @@ class SearchEngineUrl(BaseType): url.errorString())) -class KeyBindingName(BaseType): - - """The name (keys) of a keybinding.""" - - def validate(self, value): - if not value: - if self.none_ok: - return - else: - raise ValidationError(value, "may not be empty!") - - -class KeyBinding(Command): - - """The command of a keybinding.""" - - pass - - class Encoding(BaseType): """Setting for a python encoding.""" diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py index 9e8cd2dd9..6e7bd55d4 100644 --- a/qutebrowser/config/keyconfparser.py +++ b/qutebrowser/config/keyconfparser.py @@ -19,10 +19,10 @@ """Parser for the key configuration.""" - +import collections import os.path -from qutebrowser.config import configdata +from qutebrowser.config import configdata, textwrapper from qutebrowser.commands import cmdutils from qutebrowser.utils import log @@ -56,12 +56,67 @@ class KeyConfigParser: self._cur_section = None self._cur_command = None # Mapping of section name(s) to keybinding -> command dicts. - self.keybindings = {} + self.keybindings = collections.OrderedDict() if not os.path.exists(self._configfile): - log.init.debug("Creating initial keybinding config.") - with open(self._configfile, 'w', encoding='utf-8') as f: - f.write(configdata.KEYBINDINGS) - self._read() + self._load_default() + else: + self._read() + log.init.debug("Loaded bindings: {}".format(self.keybindings)) + + def __str__(self): + """Get the config as string.""" + lines = configdata.KEY_FIRST_COMMENT.strip('\n').splitlines() + lines.append('') + for sectname, sect in self.keybindings.items(): + lines.append('[{}]'.format(sectname)) + lines += self._str_section_desc(sectname) + lines.append('') + data = collections.OrderedDict() + for key, cmd in sect.items(): + if cmd in data: + data[cmd].append(key) + else: + data[cmd] = [key] + for cmd, keys in data.items(): + lines.append(cmd) + for k in keys: + lines.append(' ' * 4 + k) + lines.append('') + return '\n'.join(lines) + '\n' + + def _str_section_desc(self, sectname): + """Get the section description string for sectname.""" + wrapper = textwrapper.TextWrapper() + lines = [] + try: + seclines = configdata.KEY_SECTION_DESC[sectname].splitlines() + except KeyError: + return [] + else: + for secline in seclines: + if 'http://' in secline or 'https://' in secline: + lines.append('# ' + secline) + else: + lines += wrapper.wrap(secline) + return lines + + def save(self): + """Save the key config file.""" + log.destroy.debug("Saving key config to {}".format(self._configfile)) + with open(self._configfile, 'w', encoding='utf-8') as f: + f.write(str(self)) + + def _normalize_sectname(self, s): + """Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'.""" + return ','.join(sorted(s.split(','))) + + def _load_default(self): + """Load the built-in default keybindings.""" + for sectname, sect in configdata.KEY_DATA.items(): + sectname = self._normalize_sectname(sectname) + for command, keychains in sect.items(): + for e in keychains: + self._add_binding(sectname, e, command) def _read(self): """Read the config file from disk and parse it.""" @@ -72,7 +127,8 @@ class KeyConfigParser: if not line.strip() or line.startswith('#'): continue elif line.startswith('[') and line.endswith(']'): - self._cur_section = line[1:-1] + sectname = line[1:-1] + self._cur_section = self._normalize_sectname(sectname) elif line.startswith((' ', '\t')): line = line.strip() self._read_keybinding(line) @@ -101,10 +157,13 @@ class KeyConfigParser: "command!".format(line)) else: assert self._cur_section is not None - if self._cur_section not in self.keybindings: - self.keybindings[self._cur_section] = {} - section_bindings = self.keybindings[self._cur_section] - section_bindings[line] = self._cur_command + self._add_binding(self._cur_section, line, self._cur_command) + + def _add_binding(self, sectname, keychain, command): + """Add a new binding from keychain to command in section sectname.""" + if sectname not in self.keybindings: + self.keybindings[sectname] = collections.OrderedDict() + self.keybindings[sectname][keychain] = command def get_bindings_for(self, section): """Get a dict with all merged keybindings for a section.""" @@ -113,4 +172,8 @@ class KeyConfigParser: sects = [s.strip() for s in sectstring.split(',')] if any(s == section for s in sects): bindings.update(d) + try: + bindings.update(self.keybindings['all']) + except KeyError: + pass return bindings diff --git a/qutebrowser/config/textwrapper.py b/qutebrowser/config/textwrapper.py new file mode 100644 index 000000000..440c1a21a --- /dev/null +++ b/qutebrowser/config/textwrapper.py @@ -0,0 +1,39 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014 Florian Bruhin (The Compiler) +# +# 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 . + +"""Textwrapper used for config files.""" + +import textwrap + + +class TextWrapper(textwrap.TextWrapper): + + """Text wrapper customized to be used in configs.""" + + def __init__(self, *args, **kwargs): + kw = { + 'width': 72, + 'replace_whitespace': False, + 'break_long_words': False, + 'break_on_hyphens': False, + 'initial_indent': '# ', + 'subsequent_indent': '# ', + } + kw.update(kwargs) + super().__init__(*args, **kw) diff --git a/qutebrowser/test/config/test_configtypes.py b/qutebrowser/test/config/test_configtypes.py index aa17c9436..1c8aec76e 100644 --- a/qutebrowser/test/config/test_configtypes.py +++ b/qutebrowser/test/config/test_configtypes.py @@ -1733,32 +1733,6 @@ class SearchEngineUrlTests(unittest.TestCase): self.assertEqual(self.t.transform("foobar"), "foobar") -class KeyBindingNameTests(unittest.TestCase): - - """Test KeyBindingName.""" - - def setUp(self): - self.t = configtypes.KeyBindingName() - - def test_validate_empty(self): - """Test validate with empty string and none_ok = False.""" - with self.assertRaises(configtypes.ValidationError): - self.t.validate('') - - def test_validate_empty_none_ok(self): - """Test validate with empty string and none_ok = True.""" - t = configtypes.KeyBindingName(none_ok=True) - t.validate('') - - def test_transform_empty(self): - """Test transform with an empty value.""" - self.assertIsNone(self.t.transform('')) - - def test_transform(self): - """Test transform with a value.""" - self.assertEqual(self.t.transform("foobar"), "foobar") - - class WebSettingsFileTests(unittest.TestCase): """Test WebSettingsFile.""" diff --git a/qutebrowser/test/keyinput/test_basekeyparser.py b/qutebrowser/test/keyinput/test_basekeyparser.py index ebe806cc4..7f4c356c4 100644 --- a/qutebrowser/test/keyinput/test_basekeyparser.py +++ b/qutebrowser/test/keyinput/test_basekeyparser.py @@ -31,13 +31,15 @@ from qutebrowser.keyinput import basekeyparser from qutebrowser.test import stubs, helpers -CONFIG = {'test': {'': 'ctrla', - 'a': 'a', - 'ba': 'ba', - 'ax': 'ax', - 'ccc': 'ccc'}, - 'input': {'timeout': 100}, - 'test2': {'foo': 'bar', '': 'ctrlx'}} +CONFIG = {'input': {'timeout': 100}} + + +BINDINGS = {'test': {'': 'ctrla', + 'a': 'a', + 'ba': 'ba', + 'ax': 'ax', + 'ccc': 'ccc'}, + 'test2': {'foo': 'bar', '': 'ctrlx'}} def setUpModule(): @@ -51,6 +53,14 @@ def tearDownModule(): logging.disable(logging.NOTSET) +def _get_fake_application(): + """Construct a fake QApplication with a keyconfig.""" + app = stubs.FakeQApplication() + app.keyconfig = mock.Mock(spec=['get_bindings_for']) + app.keyconfig.get_bindings_for.side_effect = lambda s: BINDINGS[s] + return app + + class SplitCountTests(unittest.TestCase): """Test the _split_count method. @@ -99,7 +109,7 @@ class ReadConfigTests(unittest.TestCase): """Test reading the config.""" def setUp(self): - basekeyparser.config = stubs.ConfigStub(CONFIG) + basekeyparser.QCoreApplication = _get_fake_application() basekeyparser.usertypes.Timer = mock.Mock() def test_read_config_invalid(self): @@ -136,7 +146,7 @@ class SpecialKeysTests(unittest.TestCase): autospec=True) patcher.start() self.addCleanup(patcher.stop) - basekeyparser.config = stubs.ConfigStub(CONFIG) + basekeyparser.QCoreApplication = _get_fake_application() self.kp = basekeyparser.BaseKeyParser() self.kp.execute = mock.Mock() self.kp.read_config('test') @@ -171,7 +181,7 @@ class KeyChainTests(unittest.TestCase): def setUp(self): """Set up mocks and read the test config.""" - basekeyparser.config = stubs.ConfigStub(CONFIG) + basekeyparser.QCoreApplication = _get_fake_application() self.timermock = mock.Mock() basekeyparser.usertypes.Timer = mock.Mock(return_value=self.timermock) self.kp = basekeyparser.BaseKeyParser(supports_chains=True, @@ -205,6 +215,7 @@ class KeyChainTests(unittest.TestCase): def test_ambigious_keychain(self): """Test ambigious keychain.""" + basekeyparser.config = stubs.ConfigStub(CONFIG) # We start with 'a' where the keychain gives us an ambigious result. # Then we check if the timer has been set up correctly self.kp.handle(helpers.fake_keyevent(Qt.Key_A, text='a')) @@ -235,7 +246,7 @@ class CountTests(unittest.TestCase): """Test execute() with counts.""" def setUp(self): - basekeyparser.config = stubs.ConfigStub(CONFIG) + basekeyparser.QCoreApplication = _get_fake_application() basekeyparser.usertypes.Timer = mock.Mock() self.kp = basekeyparser.BaseKeyParser(supports_chains=True, supports_count=True) diff --git a/qutebrowser/test/stubs.py b/qutebrowser/test/stubs.py index a45cd4e12..df27030a8 100644 --- a/qutebrowser/test/stubs.py +++ b/qutebrowser/test/stubs.py @@ -110,8 +110,7 @@ class FakeQApplication: """Stub to insert as QApplication module.""" - def __init__(self, focus): - self.focusWidget = mock.Mock(return_value=focus) + def __init__(self): self.instance = mock.Mock(return_value=self) diff --git a/qutebrowser/test/utils/test_readline.py b/qutebrowser/test/utils/test_readline.py index adaa55c28..c45b29bb3 100644 --- a/qutebrowser/test/utils/test_readline.py +++ b/qutebrowser/test/utils/test_readline.py @@ -34,7 +34,8 @@ class NoneWidgetTests(unittest.TestCase): """Tests when the focused widget is None.""" def setUp(self): - readline.QApplication = stubs.FakeQApplication(None) + readline.QApplication = stubs.FakeQApplication() + readline.QApplication.focusWidget = mock.Mock(return_value=None) self.bridge = readline.ReadlineBridge() def test_none(self): @@ -52,7 +53,7 @@ class ReadlineBridgeTest(unittest.TestCase): def setUp(self): self.qle = mock.Mock() self.qle.__class__ = QLineEdit - readline.QApplication = stubs.FakeQApplication(self.qle) + readline.QApplication.focusWidget = mock.Mock(return_value=self.qle) self.bridge = readline.ReadlineBridge() def _set_selected_text(self, text): From 30e926abf68257cfb53c4bba8637756ebbceba75 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Sep 2014 22:29:17 +0200 Subject: [PATCH 37/89] Check duplicate keychains --- qutebrowser/config/keyconfparser.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py index 6e7bd55d4..8640ecb93 100644 --- a/qutebrowser/config/keyconfparser.py +++ b/qutebrowser/config/keyconfparser.py @@ -35,8 +35,8 @@ class KeyConfigError(Exception): lineno: The config line in which the exception occured. """ - def __init__(self): - super().__init__() + def __init__(self, msg=None): + super().__init__(msg) self.lineno = None @@ -163,6 +163,8 @@ class KeyConfigParser: """Add a new binding from keychain to command in section sectname.""" if sectname not in self.keybindings: self.keybindings[sectname] = collections.OrderedDict() + if keychain in self.get_bindings_for(sectname): + raise KeyConfigError("Duplicate keychain '{}'!".format(keychain)) self.keybindings[sectname][keychain] = command def get_bindings_for(self, section): From 95d809120580605b98ce8aff55452b267218186d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Sep 2014 22:34:20 +0200 Subject: [PATCH 38/89] Fix double bindings --- qutebrowser/config/configdata.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 55e03da9f..5ca6e45d3 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -784,7 +784,18 @@ DATA = collections.OrderedDict([ KEY_FIRST_COMMENT = """ # vim: ft=conf # -# Bindings from a key(chain) to a command. +# In this config file, qutebrowser's keybindings are configured. +# The format looks like this: +# +# [keymode] +# +# command +# keychain +# keychain2 +# ... +# +# All blank lines and lines starting with '#' are ignored. +# Inline-comments are not permitted. # # For special keys (can't be part of a keychain), enclose them in `<`...`>`. # For modifiers, you can use either `-` or `+` as delimiters, and these names: @@ -855,7 +866,7 @@ KEY_DATA = collections.OrderedDict([ ('set-cmd-text ":open -b "', ['xo']), ('set-cmd-text ":open -b {url}"', ['xO']), ('open -t about:blank', ['ga']), - ('tab-close', ['d']), + ('tab-close', ['d', '']), ('tab-only', ['co']), ('tab-focus', ['T']), ('tab-move', ['gm']), @@ -864,7 +875,7 @@ KEY_DATA = collections.OrderedDict([ ('tab-next', ['J']), ('tab-prev', ['K']), ('reload', ['r']), - ('back', ['H']), + ('back', ['H', '']), ('forward', ['L']), ('hint', ['f']), ('hint all tab', ['F']), @@ -883,7 +894,7 @@ KEY_DATA = collections.OrderedDict([ ('scroll 0 50', ['j']), ('scroll 0 -50', ['k']), ('scroll 50 0', ['l']), - ('undo', ['u']), + ('undo', ['u', '']), ('scroll-perc 0', ['gg']), ('scroll-perc', ['G']), ('search-next', ['n']), @@ -917,8 +928,6 @@ KEY_DATA = collections.OrderedDict([ ('tab-focus last', ['']), ('enter-mode passthrough', ['']), ('quit', ['']), - ('undo', ['']), - ('tab-close', ['']), ('open -t about:blank', ['']), ('scroll-page 0 1', ['']), ('scroll-page 0 -1', ['']), @@ -933,7 +942,6 @@ KEY_DATA = collections.OrderedDict([ ('tab-focus 7', ['']), ('tab-focus 8', ['']), ('tab-focus 9', ['']), - ('back', ['']), ('home', ['']), ('stop', ['']), ('print', ['']), From 277dab4069f2afa3f22aef3744726ac369482825 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Sep 2014 22:38:14 +0200 Subject: [PATCH 39/89] keyconfparser: Add empty sections. --- qutebrowser/config/keyconfparser.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py index 8640ecb93..0def7c17f 100644 --- a/qutebrowser/config/keyconfparser.py +++ b/qutebrowser/config/keyconfparser.py @@ -114,9 +114,12 @@ class KeyConfigParser: """Load the built-in default keybindings.""" for sectname, sect in configdata.KEY_DATA.items(): sectname = self._normalize_sectname(sectname) - for command, keychains in sect.items(): - for e in keychains: - self._add_binding(sectname, e, command) + if not sect: + self.keybindings[sectname] = collections.OrderedDict() + else: + for command, keychains in sect.items(): + for e in keychains: + self._add_binding(sectname, e, command) def _read(self): """Read the config file from disk and parse it.""" From 4fde56a942c6263c6bea3d4e9d6b2071cfab20bd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Sep 2014 23:05:28 +0200 Subject: [PATCH 40/89] Allow binding keys. --- qutebrowser/app.py | 5 ++- qutebrowser/config/keyconfparser.py | 55 +++++++++++++++++++++++++-- qutebrowser/keyinput/basekeyparser.py | 6 +-- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 194ce919b..65d4be0cf 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -415,9 +415,10 @@ class Application(QApplication): # config self.config.style_changed.connect(style.get_stylesheet.cache_clear) for obj in (tabs, completion, self.mainwindow, self.cmd_history, - websettings, kp[utypes.KeyMode.normal], self.modeman, - status, status.txt): + websettings, self.modeman, status, status.txt): self.config.changed.connect(obj.on_config_changed) + for obj in kp.values(): + self.keyconfig.changed.connect(obj.on_keyconfig_changed) # statusbar # FIXME some of these probably only should be triggered on mainframe diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py index 0def7c17f..d8bf7ba68 100644 --- a/qutebrowser/config/keyconfparser.py +++ b/qutebrowser/config/keyconfparser.py @@ -22,8 +22,10 @@ import collections import os.path +from PyQt5.QtCore import pyqtSignal, QObject + from qutebrowser.config import configdata, textwrapper -from qutebrowser.commands import cmdutils +from qutebrowser.commands import cmdutils, cmdexc from qutebrowser.utils import log @@ -40,17 +42,28 @@ class KeyConfigError(Exception): self.lineno = None -class KeyConfigParser: +class KeyConfigParser(QObject): - """Parser for the keybind config.""" + """Parser for the keybind config. - def __init__(self, configdir, fname): + Attributes: + FIXME + + Signals: + changed: Emitted when the config has changed. + arg: Name of the mode which was changed. + """ + + changed = pyqtSignal(str) + + def __init__(self, configdir, fname, parent=None): """Constructor. Args: configdir: The directory to save the configs in. fname: The filename of the config. """ + super().__init__(parent) self._configdir = configdir self._configfile = os.path.join(self._configdir, fname) self._cur_section = None @@ -106,6 +119,35 @@ class KeyConfigParser: with open(self._configfile, 'w', encoding='utf-8') as f: f.write(str(self)) + @cmdutils.register(instance='keyconfig') + def bind(self, key, command, mode=None): + """Bind a key to a command. + + // + + FIXME: We should use the KeyMode enum here, and some argparser type for + a comma-separated list of enums. + + Args: + key: The keychain or special key (inside <...>) to bind. + command: The command to execute. + mode: A comma-separated list of modes to bind the key in + (default: normal mode). + """ + if mode is None: + mode = 'normal' + for m in mode.split(','): + if m not in configdata.KEY_DATA: + raise cmdexc.CommandError("Invalid mode {}!".format(m)) + if command.split(maxsplit=1)[0] not in cmdutils.cmd_dict: + raise cmdexc.CommandError("Invalid command {}!".format(command)) + try: + self._add_binding(mode, key, command) + except KeyConfigError as e: + raise cmdexc.CommandError(e) + for m in mode.split(','): + self.changed.emit(m) + def _normalize_sectname(self, s): """Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'.""" return ','.join(sorted(s.split(','))) @@ -120,6 +162,7 @@ class KeyConfigParser: for command, keychains in sect.items(): for e in keychains: self._add_binding(sectname, e, command) + self.changed.emit(sectname) def _read(self): """Read the config file from disk and parse it.""" @@ -141,6 +184,8 @@ class KeyConfigParser: except KeyConfigError as e: e.lineno = i raise + for sectname in self.keybindings: + self.changed.emit(sectname) def _read_command(self, line): """Read a command from a line.""" @@ -164,6 +209,8 @@ class KeyConfigParser: def _add_binding(self, sectname, keychain, command): """Add a new binding from keychain to command in section sectname.""" + log.keyboard.debug("Adding binding {} -> {} in mode {}.".format( + keychain, command, sectname)) if sectname not in self.keybindings: self.keybindings[sectname] = collections.OrderedDict() if keychain in self.get_bindings_for(sectname): diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index bf34f5c57..bffec833e 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -345,11 +345,11 @@ class BaseKeyParser(QObject): """ raise NotImplementedError - @pyqtSlot(str, str) - def on_config_changed(self, section, _option): + @pyqtSlot(str) + def on_keyconfig_changed(self, mode): """Re-read the config if a keybinding was changed.""" if self._modename is None: raise AttributeError("on_config_changed called but no section " "defined!") - if section == self._modename: + if mode == self._modename: self.read_config() From c0e8352c9530b0f6d82e0f615fd3458610660db4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Sep 2014 23:12:55 +0200 Subject: [PATCH 41/89] Allow unbinding keys. --- qutebrowser/config/keyconfparser.py | 35 ++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py index d8bf7ba68..46654f478 100644 --- a/qutebrowser/config/keyconfparser.py +++ b/qutebrowser/config/keyconfparser.py @@ -123,11 +123,6 @@ class KeyConfigParser(QObject): def bind(self, key, command, mode=None): """Bind a key to a command. - // - - FIXME: We should use the KeyMode enum here, and some argparser type for - a comma-separated list of enums. - Args: key: The keychain or special key (inside <...>) to bind. command: The command to execute. @@ -136,6 +131,7 @@ class KeyConfigParser(QObject): """ if mode is None: mode = 'normal' + mode = self._normalize_sectname(mode) for m in mode.split(','): if m not in configdata.KEY_DATA: raise cmdexc.CommandError("Invalid mode {}!".format(m)) @@ -148,6 +144,35 @@ class KeyConfigParser(QObject): for m in mode.split(','): self.changed.emit(m) + @cmdutils.register(instance='keyconfig') + def unbind(self, key, mode=None): + """Unbind a keychain. + + Args: + key: The keychain or special key (inside <...>) to bind. + mode: A comma-separated list of modes to unbind the key in + (default: normal mode). + """ + if mode is None: + mode = 'normal' + mode = self._normalize_sectname(mode) + for m in mode.split(','): + if m not in configdata.KEY_DATA: + raise cmdexc.CommandError("Invalid mode {}!".format(m)) + try: + sect = self.keybindings[mode] + except KeyError as e: + raise cmdexc.CommandError("Can't find mode section '{}'!".format( + sect)) + try: + del sect[key] + except KeyError as e: + raise cmdexc.CommandError("Can't find binding '{}' in section " + "'{}'!".format(key, mode)) + else: + for m in mode.split(','): + self.changed.emit(m) + def _normalize_sectname(self, s): """Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'.""" return ','.join(sorted(s.split(','))) From a934b1b2d93923f15af54013ba1102782150538f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Sep 2014 23:13:43 +0200 Subject: [PATCH 42/89] Update docs --- doc/help/commands.asciidoc | 25 +++++++++++++++++ doc/help/settings.asciidoc | 56 -------------------------------------- 2 files changed, 25 insertions(+), 56 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 7ef9178d5..b1408fd6b 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -6,6 +6,7 @@ |============== |Command|Description |<>|Go back in the history of the current tab. +|<>|Bind a key to a command. |<>|Cancel the first/[count]th download. |<>|Download the current page. |<>|Go forward in the history of the current tab. @@ -40,6 +41,7 @@ |<>|Switch to the next tab, or switch [count] tabs forward. |<>|Close all tabs except for the current one. |<>|Switch to the previous tab, or switch [count] tabs back. +|<>|Unbind a keychain. |<>|Re-open a closed tab (optionally skipping [count] closed tabs). |<>|Yank the current URL/title to the clipboard or primary selection. |<>|Set the zoom level for the current tab. @@ -50,6 +52,18 @@ === back Go back in the history of the current tab. +[[bind]] +=== bind +Syntax: +:bind 'key' 'command' ['mode']+ + +Bind a key to a command. + +==== positional arguments +* +'key'+: The keychain or special key (inside <...>) to bind. +* +'command'+: The command to execute. +* +'mode'+: A comma-separated list of modes to bind the key in (default: normal mode). + + [[cancel-download]] === cancel-download Cancel the first/[count]th download. @@ -340,6 +354,17 @@ Close all tabs except for the current one. === tab-prev Switch to the previous tab, or switch [count] tabs back. +[[unbind]] +=== unbind +Syntax: +:unbind 'key' ['mode']+ + +Unbind a keychain. + +==== positional arguments +* +'key'+: The keychain or special key (inside <...>) to bind. +* +'mode'+: A comma-separated list of modes to unbind the key in (default: normal mode). + + [[undo]] === undo Re-open a closed tab (optionally skipping [count] closed tabs). diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index e0acf4553..8401a16bc 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -812,62 +812,6 @@ Default: +pass:[\bprev(ious)?\b,\bback\b,\bolder\b,\b[<←≪]\b,\b(<<| Definitions of search engines which can be used via the address bar. The searchengine named `DEFAULT` is used when `general -> auto-search` is true and something else than a URL was entered to be opened. Other search engines can be used via the bang-syntax, e.g. `:open qutebrowser !google`. The string `{}` will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs. -== keybind -Bindings from a key(chain) to a command. -For special keys (can't be part of a keychain), enclose them in `<`...`>`. For modifiers, you can use either `-` or `+` as delimiters, and these names: - - * Control: `Control`, `Ctrl` - * Meta: `Meta`, `Windows`, `Mod4` - * Alt: `Alt`, `Mod1` - * Shift: `Shift` - -For simple keys (no `<>`-signs), a capital letter means the key is pressed with Shift. For special keys (with `<>`-signs), you need to explicitely add `Shift-` to match a key pressed with shift. You can bind multiple commands by separating them with `;;`. - -== keybind.insert -Keybindings for insert mode. -Since normal keypresses are passed through, only special keys are supported in this mode. -Useful hidden commands to map in this section: - - * `open-editor`: Open a texteditor with the focused field. - * `leave-mode`: Leave the command mode. - -== keybind.hint -Keybindings for hint mode. -Since normal keypresses are passed through, only special keys are supported in this mode. -Useful hidden commands to map in this section: - - * `follow-hint`: Follow the currently selected hint. - * `leave-mode`: Leave the command mode. - -== keybind.passthrough -Keybindings for passthrough mode. -Since normal keypresses are passed through, only special keys are supported in this mode. -Useful hidden commands to map in this section: - - * `leave-mode`: Leave the passthrough mode. - -== keybind.command -Keybindings for command mode. -Since normal keypresses are passed through, only special keys are supported in this mode. -Useful hidden commands to map in this section: - - * `command-history-prev`: Switch to previous command in history. - * `command-history-next`: Switch to next command in history. - * `completion-item-prev`: Select previous item in completion. - * `completion-item-next`: Select next item in completion. - * `command-accept`: Execute the command currently in the commandline. - * `leave-mode`: Leave the command mode. - -== keybind.prompt -Keybindings for prompts in the status line. -You can bind normal keys in this mode, but they will be only active when a yes/no-prompt is asked. For other prompt modes, you can only bind special keys. -Useful hidden commands to map in this section: - - * `prompt-accept`: Confirm the entered value. - * `prompt-yes`: Answer yes to a yes/no question. - * `prompt-no`: Answer no to a yes/no question. - * `leave-mode`: Leave the prompt mode. - == aliases Aliases for commands. By default, no aliases are defined. Example which adds a new command `:qtb` to open qutebrowsers website: From e516589fe3e8d5dd7986f8ca36fbb066304d5383 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Sep 2014 07:18:04 +0200 Subject: [PATCH 43/89] Fix double default keybindings. --- qutebrowser/config/configdata.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 5ca6e45d3..500bd9628 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -960,16 +960,13 @@ KEY_DATA = collections.OrderedDict([ ('command', collections.OrderedDict([ ('command-history-prev', ['']), ('command-history-next', ['']), - ('completion-item-prev', ['']), - ('completion-item-prev', ['']), - ('completion-item-next', ['']), - ('completion-item-next', ['']), + ('completion-item-prev', ['', '']), + ('completion-item-next', ['', '']), ('command-accept', ['', '', '']), ])), ('prompt', collections.OrderedDict([ - ('prompt-accept', ['', '']), - ('prompt-accept', ['']), + ('prompt-accept', ['', '', '']), ('prompt-yes', ['y']), ('prompt-no', ['n']), ])), From a796482c835e2a2e44c830938c3d1e6985ead9e5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Sep 2014 07:33:52 +0200 Subject: [PATCH 44/89] Support !-keysections, don't bind leave-mode in normal mode. --- qutebrowser/config/configdata.py | 6 +++++- qutebrowser/config/keyconfparser.py | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 500bd9628..a780d9f93 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -797,6 +797,10 @@ KEY_FIRST_COMMENT = """ # All blank lines and lines starting with '#' are ignored. # Inline-comments are not permitted. # +# keymode is a comma separated list of modes in which the keybinding should be +# active. If keymode starts with !, the keybinding is active in all modes +# except the listed modes. +# # For special keys (can't be part of a keychain), enclose them in `<`...`>`. # For modifiers, you can use either `-` or `+` as delimiters, and these names: # @@ -854,7 +858,7 @@ KEY_SECTION_DESC = { KEY_DATA = collections.OrderedDict([ - ('all', collections.OrderedDict([ + ('!normal', collections.OrderedDict([ ('leave-mode', ['', '']), ])), diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py index 46654f478..108941b48 100644 --- a/qutebrowser/config/keyconfparser.py +++ b/qutebrowser/config/keyconfparser.py @@ -175,7 +175,15 @@ class KeyConfigParser(QObject): def _normalize_sectname(self, s): """Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'.""" - return ','.join(sorted(s.split(','))) + if s.startswith('!'): + inverted = True + s = s[1:] + else: + inverted = False + sections = ','.join(sorted(s.split(','))) + if inverted: + sections = '!' + sections + return sections def _load_default(self): """Load the built-in default keybindings.""" @@ -246,8 +254,14 @@ class KeyConfigParser(QObject): """Get a dict with all merged keybindings for a section.""" bindings = {} for sectstring, d in self.keybindings.items(): + if sectstring.startswith('!'): + inverted = True + sectstring = sectstring[1:] + else: + inverted = False sects = [s.strip() for s in sectstring.split(',')] - if any(s == section for s in sects): + matches = any(s == section for s in sects) + if (not inverted and matches) or (inverted and not matches): bindings.update(d) try: bindings.update(self.keybindings['all']) From ab0e6009775b031633f7f1af38dbd1157a673497 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Sep 2014 17:53:27 +0200 Subject: [PATCH 45/89] Error if unknown sections are in the config. --- qutebrowser/app.py | 2 ++ qutebrowser/config/config.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 65d4be0cf..070c65d59 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -176,6 +176,8 @@ class Application(QApplication): self) except (configtypes.ValidationError, config.NoOptionError, + config.NoSectionError, + config.UnknownSectionError, config.InterpolationSyntaxError, configparser.InterpolationError, configparser.DuplicateSectionError, diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 267177617..22bd27063 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -75,6 +75,13 @@ class InterpolationSyntaxError(ValueError): pass +class UnknownSectionError(Exception): + + """Exception raised when there was an unknwon section in the config.""" + + pass + + class ConfigManager(QObject): """Configuration manager for qutebrowser. @@ -203,7 +210,11 @@ class ConfigManager(QObject): Args: cp: The configparser instance to read the values from. """ - for sectname in self.sections.keys(): + for sectname in cp: + if sectname is not 'DEFAULT' and sectname not in self.sections: + raise UnknownSectionError("Unknown section '{}'!".format( + sectname)) + for sectname in self.sections: if sectname not in cp: continue for k, v in cp[sectname].items(): From 3d1830ec13f7b1da5bd799664ca49b682016e39a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Sep 2014 18:48:25 +0200 Subject: [PATCH 46/89] Remove explicit package_data from setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 857fb5a32..2c553b71f 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,6 @@ try: setuptools.setup( packages=setuptools.find_packages(exclude=['qutebrowser.test']), include_package_data=True, - package_data={'qutebrowser': ['html/*', 'git-commit-id']}, entry_points={'gui_scripts': ['qutebrowser = qutebrowser.qutebrowser:main']}, test_suite='qutebrowser.test', From 31334e6df3fe31c46dd4abb4c23c59bab6561171 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Sep 2014 18:50:42 +0200 Subject: [PATCH 47/89] Build manpage in PKGBUILD --- pkg/PKGBUILD.qutebrowser-git | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/PKGBUILD.qutebrowser-git b/pkg/PKGBUILD.qutebrowser-git index d84e1ee5c..ff7715c6a 100644 --- a/pkg/PKGBUILD.qutebrowser-git +++ b/pkg/PKGBUILD.qutebrowser-git @@ -10,7 +10,7 @@ url="http://www.qutebrowser.org/" license=('GPL') depends=('python>=3.4' 'python-setuptools' 'python-pyqt5>=5.2' 'qt5-base>=5.2' 'qt5-webkit>=5.2' 'libxkbcommon-x11' 'python-pypeg2' 'python-jinja') -makedepends=('python' 'python-setuptools') +makedepends=('python' 'python-setuptools' 'asciidoc') optdepends=('python-colorlog: colored logging output') options=(!emptydirs) source=('qutebrowser::git://the-compiler.org/qutebrowser') @@ -24,4 +24,6 @@ pkgver() { package() { cd "$srcdir/qutebrowser" python setup.py install --root="$pkgdir/" --optimize=1 + a2x -f manpage doc/qutebrowser.1.asciidoc + install -Dm644 doc/qutebrowser.1 "$pkgdir/usr/share/man/man1/qutebrowser.1" } From f2b10160ccf57ef619f71e0d74290756ec7c664c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Sep 2014 20:10:13 +0200 Subject: [PATCH 48/89] Handle IOError with qute://help. --- qutebrowser/network/qutescheme.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py index 6d1f33412..d77c7a788 100644 --- a/qutebrowser/network/qutescheme.py +++ b/qutebrowser/network/qutescheme.py @@ -69,10 +69,12 @@ class QuteSchemeHandler(schemehandler.SchemeHandler): return schemehandler.ErrorNetworkReply( request, errorstr, QNetworkReply.ContentNotFoundError, self.parent()) - else: - data = handler(request) - else: + try: data = handler(request) + except IOError as e: + return schemehandler.ErrorNetworkReply( + request, str(e), QNetworkReply.ContentNotFoundError, + self.parent()) return schemehandler.SpecialNetworkReply( request, data, 'text/html', self.parent()) From 16caa9ba69662a0fc47da666f1759b88b5326007 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Sep 2014 20:19:27 +0200 Subject: [PATCH 49/89] Handle qute://help more intuitively (load index.html) --- qutebrowser/network/qutescheme.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py index d77c7a788..49977f363 100644 --- a/qutebrowser/network/qutescheme.py +++ b/qutebrowser/network/qutescheme.py @@ -128,6 +128,9 @@ class QuteHandlers: @classmethod def help(cls, request): """Handler for qute:help. Return HTML content as bytes.""" - path = 'html/doc/{}'.format(request.url().path()) + urlpath = request.url().path() + if not urlpath or urlpath == '/': + urlpath = 'index.html' + path = 'html/doc/{}'.format(urlpath) return utils.read_file(path).encode('UTF-8', errors='xmlcharrefreplace') From 2336b5de43cc4cc4ef3f67f24cf3d972aa226ff3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Sep 2014 20:27:20 +0200 Subject: [PATCH 50/89] Refactor qutehandlers so they are not classmethods. --- qutebrowser/network/qutescheme.py | 114 ++++++++++++++++-------------- 1 file changed, 59 insertions(+), 55 deletions(-) diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py index 49977f363..0a341dfb8 100644 --- a/qutebrowser/network/qutescheme.py +++ b/qutebrowser/network/qutescheme.py @@ -31,8 +31,7 @@ from PyQt5.QtNetwork import QNetworkReply import qutebrowser from qutebrowser.network import schemehandler -from qutebrowser.utils import version, utils, jinja -from qutebrowser.utils import log as logutils +from qutebrowser.utils import version, utils, jinja, log pyeval_output = ":pyeval was never called" @@ -56,14 +55,14 @@ class QuteSchemeHandler(schemehandler.SchemeHandler): path = request.url().path() host = request.url().host() # An url like "qute:foo" is split as "scheme:path", not "scheme:host". - logutils.misc.debug("url: {}, path: {}, host {}".format( + log.misc.debug("url: {}, path: {}, host {}".format( request.url().toDisplayString(), path, host)) try: - handler = getattr(QuteHandlers, path) - except AttributeError: + handler = HANDLERS[path] + except KeyError: try: - handler = getattr(QuteHandlers, host) - except AttributeError: + handler = HANDLERS[host] + except KeyError: errorstr = "No handler found for {}!".format( request.url().toDisplayString()) return schemehandler.ErrorNetworkReply( @@ -79,58 +78,63 @@ class QuteSchemeHandler(schemehandler.SchemeHandler): request, data, 'text/html', self.parent()) -class QuteHandlers: +def qute_pyeval(_request): + """Handler for qute:pyeval. Return HTML content as bytes.""" + html = jinja.env.get_template('pre.html').render( + title='pyeval', content=pyeval_output) + return html.encode('UTF-8', errors='xmlcharrefreplace') - """Handlers for qute:... pages.""" - @classmethod - def pyeval(cls, _request): - """Handler for qute:pyeval. Return HTML content as bytes.""" - html = jinja.env.get_template('pre.html').render( - title='pyeval', content=pyeval_output) - return html.encode('UTF-8', errors='xmlcharrefreplace') +def qute_version(_request): + """Handler for qute:version. Return HTML content as bytes.""" + html = jinja.env.get_template('version.html').render( + title='Version info', version=version.version(), + copyright=qutebrowser.__copyright__) + return html.encode('UTF-8', errors='xmlcharrefreplace') - @classmethod - def version(cls, _request): - """Handler for qute:version. Return HTML content as bytes.""" - html = jinja.env.get_template('version.html').render( - title='Version info', version=version.version(), - copyright=qutebrowser.__copyright__) - return html.encode('UTF-8', errors='xmlcharrefreplace') - @classmethod - def plainlog(cls, _request): - """Handler for qute:plainlog. Return HTML content as bytes.""" - if logutils.ram_handler is None: - text = "Log output was disabled." - else: - text = logutils.ram_handler.dump_log() - html = jinja.env.get_template('pre.html').render( - title='log', content=text) - return html.encode('UTF-8', errors='xmlcharrefreplace') +def qute_plainlog(_request): + """Handler for qute:plainlog. Return HTML content as bytes.""" + if log.ram_handler is None: + text = "Log output was disabled." + else: + text = log.ram_handler.dump_log() + html = jinja.env.get_template('pre.html').render(title='log', content=text) + return html.encode('UTF-8', errors='xmlcharrefreplace') - @classmethod - def log(cls, _request): - """Handler for qute:log. Return HTML content as bytes.""" - if logutils.ram_handler is None: - html_log = None - else: - html_log = logutils.ram_handler.dump_log(html=True) - html = jinja.env.get_template('log.html').render( - title='log', content=html_log) - return html.encode('UTF-8', errors='xmlcharrefreplace') - @classmethod - def gpl(cls, _request): - """Handler for qute:gpl. Return HTML content as bytes.""" - return utils.read_file('html/COPYING.html').encode('ASCII') +def qute_log(_request): + """Handler for qute:log. Return HTML content as bytes.""" + if log.ram_handler is None: + html_log = None + else: + html_log = log.ram_handler.dump_log(html=True) + html = jinja.env.get_template('log.html').render( + title='log', content=html_log) + return html.encode('UTF-8', errors='xmlcharrefreplace') + + +def qute_gpl(_request): + """Handler for qute:gpl. Return HTML content as bytes.""" + return utils.read_file('html/COPYING.html').encode('ASCII') + + +def qute_help(request): + """Handler for qute:help. Return HTML content as bytes.""" + urlpath = request.url().path() + if not urlpath or urlpath == '/': + urlpath = 'index.html' + path = 'html/doc/{}'.format(urlpath) + return utils.read_file(path).encode('UTF-8', errors='xmlcharrefreplace') + + +HANDLERS = { + 'pyeval': qute_pyeval, + 'version': qute_version, + 'plainlog': qute_plainlog, + 'log': qute_log, + 'gpl': qute_gpl, + 'help': qute_help, +} + - @classmethod - def help(cls, request): - """Handler for qute:help. Return HTML content as bytes.""" - urlpath = request.url().path() - if not urlpath or urlpath == '/': - urlpath = 'index.html' - path = 'html/doc/{}'.format(urlpath) - return utils.read_file(path).encode('UTF-8', - errors='xmlcharrefreplace') From 80ef0782d505829d80e3dd739f6cb0beaaf90ff7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 13 Sep 2014 00:22:27 +0200 Subject: [PATCH 51/89] Improve some docstrings. --- doc/help/commands.asciidoc | 79 +++++++++++++---------------- qutebrowser/browser/commands.py | 50 +++++++++--------- qutebrowser/browser/quickmarks.py | 8 +-- qutebrowser/commands/cmdutils.py | 3 ++ qutebrowser/config/config.py | 21 ++++---- qutebrowser/config/keyconfparser.py | 8 +-- qutebrowser/utils/utilcmds.py | 10 ++-- scripts/generate_doc.py | 2 + 8 files changed, 91 insertions(+), 90 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index b1408fd6b..bbaf2f97d 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -21,7 +21,6 @@ |<>|Open a page from the clipboard. |<>|Open a "previous" link. |<>|Print the current/[count]th tab. -|<>|Quit qutebrowser. |<>|Add a new quickmark. |<>|Load a quickmark. |<>|Save the current page as a quickmark. @@ -59,9 +58,9 @@ Syntax: +:bind 'key' 'command' ['mode']+ Bind a key to a command. ==== positional arguments -* +'key'+: The keychain or special key (inside <...>) to bind. +* +'key'+: The keychain or special key (inside `<...>`) to bind. * +'command'+: The command to execute. -* +'mode'+: A comma-separated list of modes to bind the key in (default: normal mode). +* +'mode'+: A comma-separated list of modes to bind the key in (default: `normal`). [[cancel-download]] @@ -78,13 +77,13 @@ Go forward in the history of the current tab. [[get]] === get -Syntax: +:get 'sectname' 'optname'+ +Syntax: +:get 'section' 'option'+ Get the value from a section/option. ==== positional arguments -* +'sectname'+: The section where the option is in. -* +'optname'+: The name of the option. +* +'section'+: The section where the option is in. +* +'option'+: The name of the option. [[help]] === help @@ -123,10 +122,6 @@ Start hinting. - `yank-primary`: Yank the link to the primary selection. - `fill`: Fill the commandline with the command given as argument. - - `cmd-tab`: Fill the commandline with `:open -t` and the - link. - - `cmd-tag-bg`: Fill the commandline with `:open -b` and - the link. - `rapid`: Open the link in a new tab and stay in hinting mode. - `download`: Download the link. - `userscript`: Call an userscript with `$QUTE_URL` set to the @@ -156,13 +151,13 @@ Toggle the web inspector. [[later]] === later -Syntax: +:later 'ms' 'command' ['command' ...]+ +Syntax: +:later 'ms' 'command'+ Execute a command after some time. ==== positional arguments * +'ms'+: How many milliseconds to wait. -* +'command'+: The command/args to run. +* +'command'+: The command to run. [[next-page]] === next-page @@ -170,33 +165,34 @@ Syntax: +:next-page [*--tab*]+ Open a "next" link. -This tries to automatically click on typical "Next Page" links using some heuristics. +This tries to automatically click on typical _Next Page_ links using some heuristics. ==== optional arguments -* +*-t*+, +*--tab*+: Whether to open a new tab. +* +*-t*+, +*--tab*+: Open in a new tab. [[open]] === open -Syntax: +:open [*--bg*] [*--tab*] 'urlstr'+ +Syntax: +:open [*--bg*] [*--tab*] 'url'+ Open a URL in the current/[count]th tab. ==== positional arguments -* +'urlstr'+: The URL to open, as string. +* +'url'+: The URL to open. ==== optional arguments -* +*-b*+, +*--bg*+: Whether to open in a background tab. -* +*-t*+, +*--tab*+: Whether to open in a tab. +* +*-b*+, +*--bg*+: Open in a new background tab. +* +*-t*+, +*--tab*+: Open in a new tab. [[paste]] === paste -Syntax: +:paste [*--sel*] [*--tab*]+ +Syntax: +:paste [*--sel*] [*--tab*] [*--bg*]+ Open a page from the clipboard. ==== optional arguments -* +*-s*+, +*--sel*+: True to use primary selection, False to use clipboard -* +*-t*+, +*--tab*+: True to open in a new tab. +* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. +* +*-t*+, +*--tab*+: Open in a new tab. +* +*-b*+, +*--bg*+: Open in a background tab. [[prev-page]] === prev-page @@ -204,10 +200,10 @@ Syntax: +:prev-page [*--tab*]+ Open a "previous" link. -This tries to automaticall click on typical "Previous Page" links using some heuristics. +This tries to automatically click on typical _Previous Page_ links using some heuristics. ==== optional arguments -* +*-t*+, +*--tab*+: Whether to open a new tab. +* +*-t*+, +*--tab*+: Open in a new tab. [[print]] === print @@ -216,22 +212,16 @@ Syntax: +:print [*--preview*]+ Print the current/[count]th tab. ==== optional arguments -* +*-p*+, +*--preview*+: Whether to preview instead of printing. - -[[q]] -=== q -Syntax: +:quit+ - -Quit qutebrowser. +* +*-p*+, +*--preview*+: Show preview instead of printing. [[quickmark-add]] === quickmark-add -Syntax: +:quickmark-add 'urlstr' 'name'+ +Syntax: +:quickmark-add 'url' 'name'+ Add a new quickmark. ==== positional arguments -* +'urlstr'+: The url to add as quickmark, as string. +* +'url'+: The url to add as quickmark. * +'name'+: The name for the new quickmark. [[quickmark-load]] @@ -244,8 +234,8 @@ Load a quickmark. * +'name'+: The name of the quickmark to load. ==== optional arguments -* +*-t*+, +*--tab*+: Whether to load the quickmark in a new tab. -* +*-b*+, +*--bg*+: Whether to load the quickmark in the background. +* +*-t*+, +*--tab*+: Load the quickmark in a new tab. +* +*-b*+, +*--bg*+: Load the quickmark in a new background tab. [[quickmark-save]] === quickmark-save @@ -283,17 +273,17 @@ Save the config file. [[set]] === set -Syntax: +:set [*--temp*] 'sectname' 'optname' 'value'+ +Syntax: +:set [*--temp*] 'section' 'option' 'value'+ Set an option. ==== positional arguments -* +'sectname'+: The section where the option is in. -* +'optname'+: The name of the option. +* +'section'+: The section where the option is in. +* +'option'+: The name of the option. * +'value'+: The value to set. ==== optional arguments -* +*-t*+, +*--temp*+: Set value temporarely. +* +*-t*+, +*--temp*+: Set value temporarily. [[set-cmd-text]] === set-cmd-text @@ -340,7 +330,8 @@ Syntax: +:tab-move ['direction']+ Move the current tab. ==== positional arguments -* +'direction'+: + or - for relative moving, none for absolute. +* +'direction'+: `+` or `-` for relative moving, not given for absolute moving. + [[tab-next]] === tab-next @@ -361,8 +352,8 @@ Syntax: +:unbind 'key' ['mode']+ Unbind a keychain. ==== positional arguments -* +'key'+: The keychain or special key (inside <...>) to bind. -* +'mode'+: A comma-separated list of modes to unbind the key in (default: normal mode). +* +'key'+: The keychain or special key (inside <...>) to unbind. +* +'mode'+: A comma-separated list of modes to unbind the key in (default: `normal`). [[undo]] @@ -376,8 +367,8 @@ Syntax: +:yank [*--title*] [*--sel*]+ Yank the current URL/title to the clipboard or primary selection. ==== optional arguments -* +*-t*+, +*--title*+: Whether to yank the title instead of the URL. -* +*-s*+, +*--sel*+: True to use primary selection, False to use clipboard +* +*-t*+, +*--title*+: Yank the title instead of the URL. +* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. [[zoom]] === zoom @@ -600,7 +591,7 @@ The percentage can be given either as argument or as count. If no percentage is * +'perc'+: Percentage to scroll. ==== optional arguments -* +*-x*+, +*--horizontal*+: Whether to scroll horizontally. +* +*-x*+, +*--horizontal*+: Scroll horizontally instead of vertically. [[search-next]] === search-next diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 2ee04a72c..97a5f38b1 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -159,17 +159,17 @@ class CommandDispatcher: @cmdutils.register(instance='mainwindow.tabs.cmd', name='open', split=False) - def openurl(self, urlstr, bg=False, tab=False, count=None): + def openurl(self, url, bg=False, tab=False, count=None): """Open a URL in the current/[count]th tab. Args: - urlstr: The URL to open, as string. - bg: Whether to open in a background tab. - tab: Whether to open in a tab. + url: The URL to open. + bg: Open in a new background tab. + tab: Open in a new tab. count: The tab index to open the URL in, or None. """ try: - url = urlutils.fuzzy_url(urlstr) + url = urlutils.fuzzy_url(url) except urlutils.FuzzyUrlError as e: raise cmdexc.CommandError(e) if tab: @@ -216,7 +216,7 @@ class CommandDispatcher: """Print the current/[count]th tab. Args: - preview: Whether to preview instead of printing. + preview: Show preview instead of printing. count: The tab index to print, or None. """ if not qtutils.check_print_compat(): @@ -276,10 +276,6 @@ class CommandDispatcher: - `yank-primary`: Yank the link to the primary selection. - `fill`: Fill the commandline with the command given as argument. - - `cmd-tab`: Fill the commandline with `:open -t` and the - link. - - `cmd-tag-bg`: Fill the commandline with `:open -b` and - the link. - `rapid`: Open the link in a new tab and stay in hinting mode. - `download`: Download the link. - `userscript`: Call an userscript with `$QUTE_URL` set to the @@ -312,11 +308,11 @@ class CommandDispatcher: def prev_page(self, tab=False): """Open a "previous" link. - This tries to automaticall click on typical "Previous Page" links using - some heuristics. + This tries to automatically click on typical _Previous Page_ links + using some heuristics. Args: - tab: Whether to open a new tab. + tab: Open in a new tab. """ self._prevnext(prev=True, newtab=tab) @@ -324,11 +320,11 @@ class CommandDispatcher: def next_page(self, tab=False): """Open a "next" link. - This tries to automatically click on typical "Next Page" links using + This tries to automatically click on typical _Next Page_ links using some heuristics. Args: - tab: Whether to open a new tab. + tab: Open in a new tab. """ self._prevnext(prev=False, newtab=tab) @@ -357,7 +353,7 @@ class CommandDispatcher: Args: perc: Percentage to scroll. - horizontal: Whether to scroll horizontally. + horizontal: Scroll horizontally instead of vertically. count: Percentage to scroll. """ self._scroll_percent(perc, count, @@ -385,8 +381,8 @@ class CommandDispatcher: """Yank the current URL/title to the clipboard or primary selection. Args: - sel: True to use primary selection, False to use clipboard - title: Whether to yank the title instead of the URL. + sel: Use the primary selection instead of the clipboard. + title: Yank the title instead of the URL. """ clipboard = QApplication.clipboard() if title: @@ -490,12 +486,13 @@ class CommandDispatcher: raise cmdexc.CommandError("Last tab") @cmdutils.register(instance='mainwindow.tabs.cmd') - def paste(self, sel=False, tab=False): + def paste(self, sel=False, tab=False, bg=False): """Open a page from the clipboard. Args: - sel: True to use primary selection, False to use clipboard - tab: True to open in a new tab. + sel: Use the primary selection instead of the clipboard. + tab: Open in a new tab. + bg: Open in a background tab. """ clipboard = QApplication.clipboard() if sel and clipboard.supportsSelection(): @@ -513,7 +510,9 @@ class CommandDispatcher: except urlutils.FuzzyUrlError as e: raise cmdexc.CommandError(e) if tab: - self._tabs.tabopen(url, explicit=True) + self._tabs.tabopen(url, background=False, explicit=True) + elif bg: + self._tabs.tabopen(url, background=True, explicit=True) else: widget = self._current_widget() widget.openurl(url) @@ -547,7 +546,8 @@ class CommandDispatcher: """Move the current tab. Args: - direction: + or - for relative moving, none for absolute. + direction: `+` or `-` for relative moving, not given for absolute + moving. count: If moving absolutely: New position (default: 0) If moving relatively: Offset. """ @@ -624,8 +624,8 @@ class CommandDispatcher: Args: name: The name of the quickmark to load. - tab: Whether to load the quickmark in a new tab. - bg: Whether to load the quickmark in the background. + tab: Load the quickmark in a new tab. + bg: Load the quickmark in a new background tab. """ urlstr = quickmarks.get(name) url = QUrl(urlstr) diff --git a/qutebrowser/browser/quickmarks.py b/qutebrowser/browser/quickmarks.py index ec223749e..fe83df188 100644 --- a/qutebrowser/browser/quickmarks.py +++ b/qutebrowser/browser/quickmarks.py @@ -71,21 +71,21 @@ def prompt_save(url): @cmdutils.register() -def quickmark_add(urlstr, name): +def quickmark_add(url, name): """Add a new quickmark. Args: - urlstr: The url to add as quickmark, as string. + url: The url to add as quickmark. name: The name for the new quickmark. """ if not name: raise cmdexc.CommandError("Can't set mark with empty name!") - if not urlstr: + if not url: raise cmdexc.CommandError("Can't set mark with empty URL!") def set_mark(): """Really set the quickmark.""" - marks[name] = urlstr + marks[name] = url if name in marks: message.confirm_async("Override existing quickmark?", set_mark, diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index ca15f59e8..4afdd3462 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -31,6 +31,7 @@ from qutebrowser.utils import debug as debugutils from qutebrowser.commands import command, cmdexc, argparser cmd_dict = {} +aliases = [] def check_overflow(arg, ctype): @@ -163,6 +164,7 @@ class register: # pylint: disable=invalid-name Return: The original function (unmodified). """ + global aliases, cmd_dict self.func = func names = self._get_names() log.commands.vdebug("Registering command {}".format(names[0])) @@ -186,6 +188,7 @@ class register: # pylint: disable=invalid-name opt_args=self.opt_args, pos_args=self.pos_args) for name in names: cmd_dict[name] = cmd + aliases += names[1:] return func def _get_names(self): diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 22bd27063..e135d52cf 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -283,7 +283,7 @@ class ConfigManager(QObject): @cmdutils.register(name='get', instance='config', completion=[Completion.section, Completion.option]) - def get_wrapper(self, sectname, optname): + def get_command(self, section, option): """Get the value from a section/option. // @@ -291,16 +291,16 @@ class ConfigManager(QObject): Wrapper for the get-command to output the value in the status bar. Args: - sectname: The section where the option is in. - optname: The name of the option. + section: The section where the option is in. + option: The name of the option. """ try: - val = self.get(sectname, optname, transformed=False) + val = self.get(section, option, transformed=False) except (NoOptionError, NoSectionError) as e: raise cmdexc.CommandError("get: {} - {}".format( e.__class__.__name__, e)) else: - message.info("{} {} = {}".format(sectname, optname, val), + message.info("{} {} = {}".format(section, option, val), immediately=True) @functools.lru_cache() @@ -336,7 +336,7 @@ class ConfigManager(QObject): @cmdutils.register(name='set', instance='config', completion=[Completion.section, Completion.option, Completion.value]) - def set_command(self, sectname, optname, value, temp=False): + def set_command(self, section, option, value, temp=False): """Set an option. // @@ -344,13 +344,14 @@ class ConfigManager(QObject): Wrapper for self.set() to output exceptions in the status bar. Args: - sectname: The section where the option is in. - optname: The name of the option. + section: The section where the option is in. + option: The name of the option. value: The value to set. - temp: Set value temporarely. + temp: Set value temporarily. """ try: - self.set('temp' if temp else 'conf', sectname, optname, value) + layer = 'temp' if temp else 'conf' + self.set(layer, section, option, value) except (NoOptionError, NoSectionError, configtypes.ValidationError, ValueError) as e: raise cmdexc.CommandError("set: {} - {}".format( diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py index 108941b48..79efb6b0b 100644 --- a/qutebrowser/config/keyconfparser.py +++ b/qutebrowser/config/keyconfparser.py @@ -124,10 +124,10 @@ class KeyConfigParser(QObject): """Bind a key to a command. Args: - key: The keychain or special key (inside <...>) to bind. + key: The keychain or special key (inside `<...>`) to bind. command: The command to execute. mode: A comma-separated list of modes to bind the key in - (default: normal mode). + (default: `normal`). """ if mode is None: mode = 'normal' @@ -149,9 +149,9 @@ class KeyConfigParser(QObject): """Unbind a keychain. Args: - key: The keychain or special key (inside <...>) to bind. + key: The keychain or special key (inside <...>) to unbind. mode: A comma-separated list of modes to unbind the key in - (default: normal mode). + (default: `normal`). """ if mode is None: mode = 'normal' diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py index 4fd1435ce..38e635861 100644 --- a/qutebrowser/utils/utilcmds.py +++ b/qutebrowser/utils/utilcmds.py @@ -19,6 +19,7 @@ """Misc. utility commands exposed to the user.""" +import shlex import types import functools @@ -41,12 +42,12 @@ def init(): @cmdutils.register() -def later(ms: int, *command: {'nargs': '+'}): +def later(ms: int, command): """Execute a command after some time. Args: ms: How many milliseconds to wait. - command: The command/args to run. + command: The command to run. """ timer = usertypes.Timer(name='later') timer.setSingleShot(True) @@ -58,7 +59,10 @@ def later(ms: int, *command: {'nargs': '+'}): raise cmdexc.CommandError("Numeric argument is too large for internal " "int representation.") _timers.append(timer) - cmdline = ' '.join(command) + try: + cmdline = shlex.split(command) + except ValueError as e: + raise cmdexc.CommandError("Could not split command: {}".format(e)) timer.timeout.connect(functools.partial( _commandrunner.run_safely, cmdline)) timer.timeout.connect(lambda: _timers.remove(timer)) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 0a104b303..cee4bcdda 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -239,6 +239,8 @@ def generate_commands(filename): hidden_cmds = [] debug_cmds = [] for name, cmd in cmdutils.cmd_dict.items(): + if name in cmdutils.aliases: + continue if cmd.hide: hidden_cmds.append((name, cmd)) elif cmd.debug: From 38c341e3ea4c0feb89e91085111563ff37aa25de Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 13 Sep 2014 00:33:15 +0200 Subject: [PATCH 52/89] Add count info in commands. --- doc/help/commands.asciidoc | 61 ++++++++++++++++++++++++++++++++ qutebrowser/browser/downloads.py | 6 +++- scripts/generate_doc.py | 5 +++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index bbaf2f97d..682bb85c3 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -51,6 +51,9 @@ === back Go back in the history of the current tab. +==== count +How many pages to go back. + [[bind]] === bind Syntax: +:bind 'key' 'command' ['mode']+ @@ -67,6 +70,9 @@ Bind a key to a command. === cancel-download Cancel the first/[count]th download. +==== count +The index of the download to cancel. + [[download-page]] === download-page Download the current page. @@ -75,6 +81,9 @@ Download the current page. === forward Go forward in the history of the current tab. +==== count +How many pages to go forward. + [[get]] === get Syntax: +:get 'section' 'option'+ @@ -183,6 +192,9 @@ Open a URL in the current/[count]th tab. * +*-b*+, +*--bg*+: Open in a new background tab. * +*-t*+, +*--tab*+: Open in a new tab. +==== count +The tab index to open the URL in, or None. + [[paste]] === paste Syntax: +:paste [*--sel*] [*--tab*] [*--bg*]+ @@ -214,6 +226,9 @@ Print the current/[count]th tab. ==== optional arguments * +*-p*+, +*--preview*+: Show preview instead of printing. +==== count +The tab index to print, or None. + [[quickmark-add]] === quickmark-add Syntax: +:quickmark-add 'url' 'name'+ @@ -249,6 +264,9 @@ Quit qutebrowser. === reload Reload the current/[count]th tab. +==== count +The tab index to reload, or None. + [[report]] === report Report a bug in qutebrowser. @@ -309,10 +327,16 @@ Note the {url} variable which gets replaced by the current URL might be useful h === stop Stop loading in the current/[count]th tab. +==== count +The tab index to stop, or None. + [[tab-close]] === tab-close Close the current/[count]th tab. +==== count +The tab index to close, or None + [[tab-focus]] === tab-focus Syntax: +:tab-focus ['index']+ @@ -323,6 +347,9 @@ Select the tab given as argument/[count]. * +'index'+: The tab index to focus, starting with 1. The special value `last` focuses the last focused tab. +==== count +The tab index to focus, starting with 1. + [[tab-move]] === tab-move Syntax: +:tab-move ['direction']+ @@ -333,10 +360,17 @@ Move the current tab. * +'direction'+: `+` or `-` for relative moving, not given for absolute moving. +==== count +If moving absolutely: New position (default: 0) If moving relatively: Offset. + + [[tab-next]] === tab-next Switch to the next tab, or switch [count] tabs forward. +==== count +How many tabs to switch forward. + [[tab-only]] === tab-only Close all tabs except for the current one. @@ -345,6 +379,9 @@ Close all tabs except for the current one. === tab-prev Switch to the previous tab, or switch [count] tabs back. +==== count +How many tabs to switch back. + [[unbind]] === unbind Syntax: +:unbind 'key' ['mode']+ @@ -381,14 +418,23 @@ The zoom can be given as argument or as [count]. If neither of both is given, th ==== positional arguments * +'zoom'+: The zoom percentage to set. +==== count +The zoom percentage to set. + [[zoom-in]] === zoom-in Increase the zoom level for the current tab. +==== count +How many steps to zoom in. + [[zoom-out]] === zoom-out Decrease the zoom level for the current tab. +==== count +How many steps to zoom out. + == Hidden commands .Quick reference @@ -569,6 +615,9 @@ Scroll the current tab by 'count * dx/dy'. * +'dx'+: How much to scroll in x-direction. * +'dy'+: How much to scroll in x-direction. +==== count +multiplier + [[scroll-page]] === scroll-page Syntax: +:scroll-page 'x' 'y'+ @@ -579,6 +628,9 @@ Scroll the frame page-wise. * +'x'+: How many pages to scroll to the right. * +'y'+: How many pages to scroll down. +==== count +multiplier + [[scroll-perc]] === scroll-perc Syntax: +:scroll-perc [*--horizontal*] ['perc']+ @@ -593,14 +645,23 @@ The percentage can be given either as argument or as count. If no percentage is ==== optional arguments * +*-x*+, +*--horizontal*+: Scroll horizontally instead of vertically. +==== count +Percentage to scroll. + [[search-next]] === search-next Continue the search to the ([count]th) next term. +==== count +How many elements to ignore. + [[search-prev]] === search-prev Continue the search to the ([count]th) previous term. +==== count +How many elements to ignore. + == Debugging commands These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag. diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 2c7f9129d..3623be224 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -359,7 +359,11 @@ class DownloadManager(QObject): @cmdutils.register(instance='downloadmanager') def cancel_download(self, count=1): - """Cancel the first/[count]th download.""" + """Cancel the first/[count]th download. + + Args: + count: The index of the download to cancel. + """ if count == 0: return try: diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index cee4bcdda..c292e45fd 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -181,6 +181,11 @@ def _get_command_doc(name, cmd): raise KeyError("No description for arg {} of command " "'{}'!".format(e, cmd.name)) + if cmd.count: + output.append("") + output.append("==== count") + output.append(parser.arg_descs['count']) + output.append("") output.append("") return '\n'.join(output) From fea3524443141bd9d62d255e0fabfed01856dc7f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 13 Sep 2014 00:37:07 +0200 Subject: [PATCH 53/89] Remove 'or None' in docs. --- doc/help/commands.asciidoc | 10 +++++----- qutebrowser/utils/utils.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 682bb85c3..f6de403ee 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -193,7 +193,7 @@ Open a URL in the current/[count]th tab. * +*-t*+, +*--tab*+: Open in a new tab. ==== count -The tab index to open the URL in, or None. +The tab index to open the URL in. [[paste]] === paste @@ -227,7 +227,7 @@ Print the current/[count]th tab. * +*-p*+, +*--preview*+: Show preview instead of printing. ==== count -The tab index to print, or None. +The tab index to print. [[quickmark-add]] === quickmark-add @@ -265,7 +265,7 @@ Quit qutebrowser. Reload the current/[count]th tab. ==== count -The tab index to reload, or None. +The tab index to reload. [[report]] === report @@ -328,14 +328,14 @@ Note the {url} variable which gets replaced by the current URL might be useful h Stop loading in the current/[count]th tab. ==== count -The tab index to stop, or None. +The tab index to stop. [[tab-close]] === tab-close Close the current/[count]th tab. ==== count -The tab index to close, or None +The tab index to close [[tab-focus]] === tab-focus diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index b5af9e798..fd7e35863 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -543,7 +543,7 @@ class DocstringParser: if stop: break for k, v in self.arg_descs.items(): - self.arg_descs[k] = ' '.join(v) + self.arg_descs[k] = ' '.join(v).replace(', or None', '') self.long_desc = ' '.join(self.long_desc) self.short_desc = ' '.join(self.short_desc) From ef31157f5e88f9eafd3f45b0cbf3b0df324a677b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 22:09:01 +0200 Subject: [PATCH 54/89] cmdutils: Use inspect.getdoc --- qutebrowser/commands/cmdutils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 4afdd3462..8f094d98b 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -225,8 +225,9 @@ class register: # pylint: disable=invalid-name raise ValueError("{} is a class method, but instance was not " "given!".format(self.name[0])) has_count = 'count' in signature.parameters - if self.func.__doc__ is not None: - desc = self.func.__doc__.splitlines()[0].strip() + doc = inspect.getdoc(self.func) + if doc is not None: + desc = doc.splitlines()[0].strip() else: desc = "" if not self.ignore_args: From 319ea242da815c0a2d13009f455f281fcfea68d4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 22:46:48 +0200 Subject: [PATCH 55/89] cmdutils: raise TypeError instead of ValueError. --- qutebrowser/commands/cmdutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 8f094d98b..5e77aeaca 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -222,8 +222,8 @@ class register: # pylint: disable=invalid-name type_conv = {} signature = inspect.signature(self.func) if 'self' in signature.parameters and self.instance is None: - raise ValueError("{} is a class method, but instance was not " - "given!".format(self.name[0])) + raise TypeError("{} is a class method, but instance was not " + "given!".format(self.name[0])) has_count = 'count' in signature.parameters doc = inspect.getdoc(self.func) if doc is not None: From cd8d137dd6bdd6df9dcfab5ec5ad910a101deae7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 22:47:18 +0200 Subject: [PATCH 56/89] cmdutils: Bail out if instance is given but function takes no self. --- qutebrowser/commands/cmdutils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 5e77aeaca..398bbed32 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -224,6 +224,9 @@ class register: # pylint: disable=invalid-name if 'self' in signature.parameters and self.instance is None: raise TypeError("{} is a class method, but instance was not " "given!".format(self.name[0])) + elif 'self' not in signature.parameters and self.instance is not None: + raise TypeError("{} is not a class method, but instance was " + "given!".format(self.name[0])) has_count = 'count' in signature.parameters doc = inspect.getdoc(self.func) if doc is not None: From 4b759c5513b6ba115621e28a721f4ed0d34176f5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 22:48:00 +0200 Subject: [PATCH 57/89] cmdutils: Make sure functions don't have **kwargs. --- qutebrowser/commands/cmdutils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 398bbed32..0b5e285b5 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -227,6 +227,9 @@ class register: # pylint: disable=invalid-name elif 'self' not in signature.parameters and self.instance is not None: raise TypeError("{} is not a class method, but instance was " "given!".format(self.name[0])) + elif inspect.getfullargspec(self.func).varkw is not None: + raise TypeError("{}: functions with varkw arguments are not " + "supported!".format(self.name[0])) has_count = 'count' in signature.parameters doc = inspect.getdoc(self.func) if doc is not None: From d4f584684bc2d1ef94b6f0f45d43df66c8d16680 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 22:48:25 +0200 Subject: [PATCH 58/89] command: Iterate over function signature when calling instead over namespace. --- qutebrowser/commands/command.py | 73 ++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 490a3e558..39a4a8ed8 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -19,6 +19,8 @@ """Contains the Command class, a skeleton for a command.""" +import inspect + from PyQt5.QtCore import QCoreApplication from PyQt5.QtWebKit import QWebSettings @@ -126,36 +128,51 @@ class Command: e.status, e)) return - for name, arg in vars(namespace).items(): - if isinstance(arg, list): - # If we got a list, we assume that's our *args, so we don't add - # it to kwargs. - # FIXME: This approach is rather naive, but for now it works. - posargs += arg + signature = inspect.signature(self.handler) + + for i, param in enumerate(signature.parameters.values()): + if i == 0 and self.instance is not None: + # Special case for 'self'. + assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + if self.instance == '': + obj = app + else: + obj = utils.dotted_getattr(app, self.instance) + posargs.append(obj) + continue + elif param.name == 'count': + # Special case for 'count'. + if not self.count: + raise TypeError("{}: count argument given with a command " + "which does not support count!".format( + self.name)) + if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: + posargs.append(count) + elif param.kind == inspect.Parameter.KEYWORD_ONLY: + kwargs['count'] = count + else: + raise TypeError("{}: invalid parameter type {} for " + "argument 'count'!".format( + self.name, param.kind)) + continue + value = getattr(namespace, param.name) + if param.name in self.type_conv: + # We convert enum types after getting the values from + # argparse, because argparse's choices argument is + # processed after type conversation, which is not what we + # want. + value = self.type_conv[param.name](value) + if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: + posargs.append(value) + elif param.kind == inspect.Parameter.VAR_POSITIONAL: + posargs += value + elif param.kind == inspect.Parameter.KEYWORD_ONLY: + kwargs[param.name] = value else: - if name in self.type_conv: - # We convert enum types after getting the values from - # argparse, because argparse's choices argument is - # processed after type conversation, which is not what we - # want. - arg = self.type_conv[name](arg) - kwargs[name] = arg - - if self.instance is not None: - # Add the 'self' parameter. - if self.instance == '': - obj = app - else: - obj = utils.dotted_getattr(app, self.instance) - posargs.insert(0, obj) - - if count is not None and self.count: - kwargs = {'count': count} - + raise TypeError("{}: Invalid parameter type {} for argument " + "'{}'!".format( + self.name, param.kind, param.name)) self._check_prerequisites() log.commands.debug('Calling {}'.format( debug.format_call(self.handler, posargs, kwargs))) - # FIXME this won't work properly if some arguments are required to be - # positional, e.g.: - # def fun(one=True, two=False, *args) self.handler(*posargs, **kwargs) From b2058e2f0e62b1b8c7c4174151f9792c257aae3e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 22:56:02 +0200 Subject: [PATCH 59/89] cmdutils: Default to nargs='+' for *args. --- qutebrowser/browser/commands.py | 2 +- qutebrowser/commands/cmdutils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 97a5f38b1..fa77889d2 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -603,7 +603,7 @@ class CommandDispatcher: self.openurl(config.get('general', 'startpage')[0]) @cmdutils.register(instance='mainwindow.tabs.cmd') - def run_userscript(self, cmd, *args): + def run_userscript(self, cmd, *args : {'nargs': '*'}): """Run an userscript given as argument. Args: diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 0b5e285b5..5f95e2114 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -308,7 +308,7 @@ class register: # pylint: disable=invalid-name kwargs['type'] = typ if param.kind == inspect.Parameter.VAR_POSITIONAL: - kwargs['nargs'] = '*' + kwargs['nargs'] = '+' elif typ is not bool and param.default is not inspect.Parameter.empty: kwargs['default'] = param.default kwargs['nargs'] = '?' From f1f05516b3b7c35b10775f6cad23a5402ee8e362 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 23:06:52 +0200 Subject: [PATCH 60/89] command: Fix handling of count when it's not given. --- qutebrowser/commands/command.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 39a4a8ed8..3504edf2e 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -147,9 +147,13 @@ class Command: "which does not support count!".format( self.name)) if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: - posargs.append(count) + if count is not None: + posargs.append(count) + else: + posargs.append(param.default) elif param.kind == inspect.Parameter.KEYWORD_ONLY: - kwargs['count'] = count + if count is not None: + kwargs['count'] = count else: raise TypeError("{}: invalid parameter type {} for " "argument 'count'!".format( From 1fd8fb57a624b3fa5855b34a276e185ca2c07c66 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 23:09:01 +0200 Subject: [PATCH 61/89] Use *args for commands where possible. --- qutebrowser/browser/hints.py | 21 +++++++-------------- qutebrowser/config/keyconfparser.py | 10 +++++----- qutebrowser/utils/utilcmds.py | 10 +++------- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 5d9ca88a9..44262fa55 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -20,7 +20,6 @@ """A HintManager to draw hints over links.""" import math -import shlex import subprocess import collections @@ -453,7 +452,7 @@ class HintManager(QObject): f.contentsSizeChanged.connect(self.on_contents_size_changed) self._context.connected_frames.append(f) - def _check_args(self, target, args): + def _check_args(self, target, *args): """Check the arguments passed to start() and raise if they're wrong. Args: @@ -463,11 +462,11 @@ class HintManager(QObject): if not isinstance(target, Target): raise TypeError("Target {} is no Target member!".format(target)) if target in (Target.userscript, Target.spawn, Target.fill): - if args is None: + if not args: raise cmdexc.CommandError( "'args' is required with target userscript/spawn/fill.") else: - if args is not None: + if args: raise cmdexc.CommandError( "'args' is only allowed with target userscript/spawn.") @@ -513,7 +512,7 @@ class HintManager(QObject): self.openurl.emit(url, newtab) def start(self, mainframe, baseurl, group=webelem.Group.all, - target=Target.normal, args=None): + target=Target.normal, *args): """Start hinting. Args: @@ -521,12 +520,12 @@ class HintManager(QObject): baseurl: URL of the current page. group: Which group of elements to hint. target: What to do with the link. See attribute docstring. - args: Arguments for userscript/download + *args: Arguments for userscript/download Emit: hint_strings_updated: Emitted to update keypraser. """ - self._check_args(target, args) + self._check_args(target, *args) if mainframe is None: # This should never happen since we check frame before calling # start. But since we had a bug where frame is None in @@ -536,13 +535,7 @@ class HintManager(QObject): self._context.target = target self._context.baseurl = baseurl self._context.frames = webelem.get_child_frames(mainframe) - if args is None: - self._context.args = None - else: - try: - self._context.args = shlex.split(args) - except ValueError as e: - raise cmdexc.CommandError("Could not split args: {}".format(e)) + self._context.args = args self._init_elements(mainframe, group) message.instance().set_text(self.HINT_TEXTS[target]) self._connect_frame_signals() diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py index 79efb6b0b..f4588f6b5 100644 --- a/qutebrowser/config/keyconfparser.py +++ b/qutebrowser/config/keyconfparser.py @@ -120,12 +120,12 @@ class KeyConfigParser(QObject): f.write(str(self)) @cmdutils.register(instance='keyconfig') - def bind(self, key, command, mode=None): + def bind(self, key, *command, mode=None): """Bind a key to a command. Args: key: The keychain or special key (inside `<...>`) to bind. - command: The command to execute. + *command: The command to execute, with optional args. mode: A comma-separated list of modes to bind the key in (default: `normal`). """ @@ -135,10 +135,10 @@ class KeyConfigParser(QObject): for m in mode.split(','): if m not in configdata.KEY_DATA: raise cmdexc.CommandError("Invalid mode {}!".format(m)) - if command.split(maxsplit=1)[0] not in cmdutils.cmd_dict: - raise cmdexc.CommandError("Invalid command {}!".format(command)) + if command[0] not in cmdutils.cmd_dict: + raise cmdexc.CommandError("Invalid command {}!".format(command[0])) try: - self._add_binding(mode, key, command) + self._add_binding(mode, key, *command) except KeyConfigError as e: raise cmdexc.CommandError(e) for m in mode.split(','): diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py index 38e635861..b2565306a 100644 --- a/qutebrowser/utils/utilcmds.py +++ b/qutebrowser/utils/utilcmds.py @@ -19,7 +19,6 @@ """Misc. utility commands exposed to the user.""" -import shlex import types import functools @@ -42,12 +41,12 @@ def init(): @cmdutils.register() -def later(ms: int, command): +def later(ms: int, *command): """Execute a command after some time. Args: ms: How many milliseconds to wait. - command: The command to run. + *command: The command to run, with optional args. """ timer = usertypes.Timer(name='later') timer.setSingleShot(True) @@ -59,10 +58,7 @@ def later(ms: int, command): raise cmdexc.CommandError("Numeric argument is too large for internal " "int representation.") _timers.append(timer) - try: - cmdline = shlex.split(command) - except ValueError as e: - raise cmdexc.CommandError("Could not split command: {}".format(e)) + cmdline = ' '.join(command) timer.timeout.connect(functools.partial( _commandrunner.run_safely, cmdline)) timer.timeout.connect(lambda: _timers.remove(timer)) From a3f722e15136eeed4bafee2ba5a02f3d35cf5cbd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 23:11:11 +0200 Subject: [PATCH 62/89] docs update --- doc/help/commands.asciidoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index f6de403ee..1814eaa4c 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -56,13 +56,13 @@ How many pages to go back. [[bind]] === bind -Syntax: +:bind 'key' 'command' ['mode']+ +Syntax: +:bind 'key' 'command' ['command' ...] ['mode']+ Bind a key to a command. ==== positional arguments * +'key'+: The keychain or special key (inside `<...>`) to bind. -* +'command'+: The command to execute. +* +'command'+: The command to execute, with optional args. * +'mode'+: A comma-separated list of modes to bind the key in (default: `normal`). @@ -160,13 +160,13 @@ Toggle the web inspector. [[later]] === later -Syntax: +:later 'ms' 'command'+ +Syntax: +:later 'ms' 'command' ['command' ...]+ Execute a command after some time. ==== positional arguments * +'ms'+: How many milliseconds to wait. -* +'command'+: The command to run. +* +'command'+: The command to run, with optional args. [[next-page]] === next-page @@ -314,7 +314,7 @@ Preset the statusbar to some text. [[spawn]] === spawn -Syntax: +:spawn ['args' ['args' ...]]+ +Syntax: +:spawn 'args' ['args' ...]+ Spawn a command in a shell. From b9216bca157ad799279b301111228c598de6d4b5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 23:16:35 +0200 Subject: [PATCH 63/89] Fix hint command arguments. --- qutebrowser/browser/commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index fa77889d2..dfd9fbd45 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -257,7 +257,7 @@ class CommandDispatcher: @cmdutils.register(instance='mainwindow.tabs.cmd') def hint(self, group=webelem.Group.all, target=hints.Target.normal, - args=None): + *args : {'nargs': '*'}): """Start hinting. Args: @@ -282,7 +282,7 @@ class CommandDispatcher: link. - `spawn`: Spawn a command. - args: Arguments for spawn/userscript/fill. + *args: Arguments for spawn/userscript/fill. - With `spawn`: The executable and arguments to spawn. `{hint-url}` will get replaced by the selected @@ -297,7 +297,7 @@ class CommandDispatcher: if frame is None: raise cmdexc.CommandError("No frame focused!") widget.hintmanager.start(frame, self._tabs.current_url(), group, - target, args) + target, *args) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) def follow_hint(self): From fe080526415db11896b0b8e2084630bd21e85734 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 23:16:54 +0200 Subject: [PATCH 64/89] command: Don't handle varargs if they are None --- qutebrowser/commands/command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 3504edf2e..f6791b0aa 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -169,7 +169,8 @@ class Command: if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: posargs.append(value) elif param.kind == inspect.Parameter.VAR_POSITIONAL: - posargs += value + if value is not None: + posargs += value elif param.kind == inspect.Parameter.KEYWORD_ONLY: kwargs[param.name] = value else: From 66f0aa8d5fcb658db8a4419b7cb5faa320ec9ee1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 23:25:40 +0200 Subject: [PATCH 65/89] cmdutils: Bind keyword-only arguments as flags. --- qutebrowser/commands/cmdutils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 5f95e2114..014b5303d 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -271,7 +271,8 @@ class register: # pylint: disable=invalid-name args = [] name = annotation_info.name or param.name shortname = annotation_info.flag or param.name[0] - if self._get_type(param, annotation_info) == bool: + if (self._get_type(param, annotation_info) == bool or + param.kind == inspect.Parameter.KEYWORD_ONLY): long_flag = '--{}'.format(name) short_flag = '-{}'.format(shortname) args.append(long_flag) @@ -309,6 +310,8 @@ class register: # pylint: disable=invalid-name if param.kind == inspect.Parameter.VAR_POSITIONAL: kwargs['nargs'] = '+' + elif param.kind == inspect.Parameter.KEYWORD_ONLY: + kwargs['default'] = param.default elif typ is not bool and param.default is not inspect.Parameter.empty: kwargs['default'] = param.default kwargs['nargs'] = '?' From 7e37b657f5efdf49526d557b9b67675c5da73eaf Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 23:25:56 +0200 Subject: [PATCH 66/89] docs update --- doc/help/commands.asciidoc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 1814eaa4c..ec42906ed 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -56,14 +56,16 @@ How many pages to go back. [[bind]] === bind -Syntax: +:bind 'key' 'command' ['command' ...] ['mode']+ +Syntax: +:bind [*--mode* 'MODE'] 'key' 'command' ['command' ...]+ Bind a key to a command. ==== positional arguments * +'key'+: The keychain or special key (inside `<...>`) to bind. * +'command'+: The command to execute, with optional args. -* +'mode'+: A comma-separated list of modes to bind the key in (default: `normal`). + +==== optional arguments +* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`). [[cancel-download]] @@ -109,7 +111,7 @@ Show help about a command or setting. [[hint]] === hint -Syntax: +:hint ['group'] ['target'] ['args']+ +Syntax: +:hint ['group'] ['target'] ['args' ['args' ...]]+ Start hinting. From 063be350e482b0293481b68be7be6e07e2734943 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 23:34:55 +0200 Subject: [PATCH 67/89] cmdutils: Merge _param_to_argparse_args and _param_to_argparse_kw. --- qutebrowser/commands/cmdutils.py | 100 ++++++++++++++++--------------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 014b5303d..3f771b87f 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -240,13 +240,14 @@ class register: # pylint: disable=invalid-name for param in signature.parameters.values(): if param.name in ('self', 'count'): continue - args = [] - kwargs = {} + argparse_args = [] + argparse_kwargs = {} annotation_info = self._parse_annotation(param) - kwargs.update(self._param_to_argparse_kw( - param, annotation_info)) - kwargs.update(annotation_info.kwargs) - args += self._param_to_argparse_pos(param, annotation_info) + args, kwargs = self._param_to_argparse_args( + param, annotation_info) + argparse_args += args + argparse_kwargs.update(kwargs) + argparse_kwargs.update(annotation_info.kwargs) typ = self._get_type(param, annotation_info) if utils.is_enum(typ): type_conv[param.name] = argparser.enum_getter(typ) @@ -254,25 +255,60 @@ class register: # pylint: disable=invalid-name if param.default is not inspect.Parameter.empty: typ = typ + (type(param.default),) type_conv[param.name] = argparser.multitype_conv(typ) - callsig = debugutils.format_call(self.parser.add_argument, - args, kwargs, full=False) + callsig = debugutils.format_call( + self.parser.add_argument, argparse_args, argparse_kwargs, + full=False) log.commands.vdebug('Adding arg {} of type {} -> {}'.format( param.name, typ, callsig)) - self.parser.add_argument(*args, **kwargs) + self.parser.add_argument(*argparse_args, **argparse_kwargs) return has_count, desc, type_conv - def _param_to_argparse_pos(self, param, annotation_info): - """Get a list of positional argparse arguments. + def _param_to_argparse_args(self, param, annotation_info): + """Get argparse arguments for a parameter. + + Return: + An (args, kwargs) tuple. Args: - param: The inspect.Parameter instance for the current parameter. + param: The inspect.Parameter object to get the args for. annotation_info: An AnnotationInfo tuple for the parameter. """ + + ParamType = usertypes.enum('ParamType', 'flag', 'positional') + + kwargs = {} + typ = self._get_type(param, annotation_info) + param_type = ParamType.positional + + try: + kwargs['help'] = self.docparser.arg_descs[param.name] + except KeyError: + pass + + if isinstance(typ, tuple): + pass + elif utils.is_enum(typ): + kwargs['choices'] = [e.name.replace('_', '-') for e in typ] + kwargs['metavar'] = param.name + elif typ is bool: + param_type = ParamType.flag + kwargs['action'] = 'store_true' + elif typ is not None: + kwargs['type'] = typ + + if param.kind == inspect.Parameter.VAR_POSITIONAL: + kwargs['nargs'] = '+' + elif param.kind == inspect.Parameter.KEYWORD_ONLY: + param_type = ParamType.flag + kwargs['default'] = param.default + elif typ is not bool and param.default is not inspect.Parameter.empty: + kwargs['default'] = param.default + kwargs['nargs'] = '?' + args = [] name = annotation_info.name or param.name shortname = annotation_info.flag or param.name[0] - if (self._get_type(param, annotation_info) == bool or - param.kind == inspect.Parameter.KEYWORD_ONLY): + if param_type == ParamType.flag: long_flag = '--{}'.format(name) short_flag = '-{}'.format(shortname) args.append(long_flag) @@ -281,42 +317,8 @@ class register: # pylint: disable=invalid-name else: args.append(name) self.pos_args.append(name) - return args - def _param_to_argparse_kw(self, param, annotation_info): - """Get argparse keyword arguments for a parameter. - - Args: - param: The inspect.Parameter object to get the args for. - annotation_info: An AnnotationInfo tuple for the parameter. - """ - kwargs = {} - - try: - kwargs['help'] = self.docparser.arg_descs[param.name] - except KeyError: - pass - typ = self._get_type(param, annotation_info) - - if isinstance(typ, tuple): - pass - elif utils.is_enum(typ): - kwargs['choices'] = [e.name.replace('_', '-') for e in typ] - kwargs['metavar'] = param.name - elif typ is bool: - kwargs['action'] = 'store_true' - elif typ is not None: - kwargs['type'] = typ - - if param.kind == inspect.Parameter.VAR_POSITIONAL: - kwargs['nargs'] = '+' - elif param.kind == inspect.Parameter.KEYWORD_ONLY: - kwargs['default'] = param.default - elif typ is not bool and param.default is not inspect.Parameter.empty: - kwargs['default'] = param.default - kwargs['nargs'] = '?' - - return kwargs + return args, kwargs def _parse_annotation(self, param): """Get argparse arguments and type from a parameter annotation. From 4d3b3616a614c8c6b0e4eb6080c82d1d2d3af67f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Sep 2014 23:56:19 +0200 Subject: [PATCH 68/89] Fix lint --- qutebrowser/browser/commands.py | 4 +- qutebrowser/commands/cmdutils.py | 61 +++++++++++++---------- qutebrowser/commands/command.py | 77 +++++++++++++++++------------ qutebrowser/config/config.py | 14 +++--- qutebrowser/config/keyconfparser.py | 4 +- qutebrowser/network/qutescheme.py | 2 - 6 files changed, 93 insertions(+), 69 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index dfd9fbd45..5f981d3de 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -257,7 +257,7 @@ class CommandDispatcher: @cmdutils.register(instance='mainwindow.tabs.cmd') def hint(self, group=webelem.Group.all, target=hints.Target.normal, - *args : {'nargs': '*'}): + *args: {'nargs': '*'}): """Start hinting. Args: @@ -603,7 +603,7 @@ class CommandDispatcher: self.openurl(config.get('general', 'startpage')[0]) @cmdutils.register(instance='mainwindow.tabs.cmd') - def run_userscript(self, cmd, *args : {'nargs': '*'}): + def run_userscript(self, cmd, *args: {'nargs': '*'}): """Run an userscript given as argument. Args: diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 3f771b87f..771819cff 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -164,7 +164,7 @@ class register: # pylint: disable=invalid-name Return: The original function (unmodified). """ - global aliases, cmd_dict + global aliases self.func = func names = self._get_names() log.commands.vdebug("Registering command {}".format(names[0])) @@ -178,6 +178,7 @@ class register: # pylint: disable=invalid-name self.parser.add_argument('-h', '--help', action=argparser.HelpAction, default=argparser.SUPPRESS, nargs=0, help=argparser.SUPPRESS) + self._check_func() has_count, desc, type_conv = self._inspect_func() cmd = command.Command( name=names[0], split=self.split, hide=self.hide, count=has_count, @@ -210,6 +211,35 @@ class register: # pylint: disable=invalid-name else: return self.name + def _check_func(self): + """Make sure the function parameters don't violate any rules.""" + signature = inspect.signature(self.func) + if 'self' in signature.parameters and self.instance is None: + raise TypeError("{} is a class method, but instance was not " + "given!".format(self.name[0])) + elif 'self' not in signature.parameters and self.instance is not None: + raise TypeError("{} is not a class method, but instance was " + "given!".format(self.name[0])) + elif inspect.getfullargspec(self.func).varkw is not None: + raise TypeError("{}: functions with varkw arguments are not " + "supported!".format(self.name[0])) + + def _get_typeconv(self, param, typ): + """Get a dict with a type conversion for the parameter. + + Args: + param: The inspect.Parameter to handle. + typ: The type of the parameter. + """ + type_conv = {} + if utils.is_enum(typ): + type_conv[param.name] = argparser.enum_getter(typ) + elif isinstance(typ, tuple): + if param.default is not inspect.Parameter.empty: + typ = typ + (type(param.default),) + type_conv[param.name] = argparser.multitype_conv(typ) + return type_conv + def _inspect_func(self): """Inspect the function to get useful informations from it. @@ -221,15 +251,6 @@ class register: # pylint: disable=invalid-name """ type_conv = {} signature = inspect.signature(self.func) - if 'self' in signature.parameters and self.instance is None: - raise TypeError("{} is a class method, but instance was not " - "given!".format(self.name[0])) - elif 'self' not in signature.parameters and self.instance is not None: - raise TypeError("{} is not a class method, but instance was " - "given!".format(self.name[0])) - elif inspect.getfullargspec(self.func).varkw is not None: - raise TypeError("{}: functions with varkw arguments are not " - "supported!".format(self.name[0])) has_count = 'count' in signature.parameters doc = inspect.getdoc(self.func) if doc is not None: @@ -240,27 +261,17 @@ class register: # pylint: disable=invalid-name for param in signature.parameters.values(): if param.name in ('self', 'count'): continue - argparse_args = [] - argparse_kwargs = {} annotation_info = self._parse_annotation(param) + typ = self._get_type(param, annotation_info) args, kwargs = self._param_to_argparse_args( param, annotation_info) - argparse_args += args - argparse_kwargs.update(kwargs) - argparse_kwargs.update(annotation_info.kwargs) - typ = self._get_type(param, annotation_info) - if utils.is_enum(typ): - type_conv[param.name] = argparser.enum_getter(typ) - elif isinstance(typ, tuple): - if param.default is not inspect.Parameter.empty: - typ = typ + (type(param.default),) - type_conv[param.name] = argparser.multitype_conv(typ) + type_conv.update(self._get_typeconv(param, typ)) callsig = debugutils.format_call( - self.parser.add_argument, argparse_args, argparse_kwargs, + self.parser.add_argument, args, kwargs, full=False) log.commands.vdebug('Adding arg {} of type {} -> {}'.format( param.name, typ, callsig)) - self.parser.add_argument(*argparse_args, **argparse_kwargs) + self.parser.add_argument(*args, **kwargs) return has_count, desc, type_conv def _param_to_argparse_args(self, param, annotation_info): @@ -317,7 +328,7 @@ class register: # pylint: disable=invalid-name else: args.append(name) self.pos_args.append(name) - + kwargs.update(annotation_info.kwargs) return args, kwargs def _parse_annotation(self, param): diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index f6791b0aa..e3f08f25f 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -98,59 +98,45 @@ class Command: raise cmdexc.PrerequisitesError( "{}: This command needs javascript enabled.".format(self.name)) - def run(self, args=None, count=None): - """Run the command. - - Note we don't catch CommandError here as it might happen async. + def _get_args(self, func, count, # noqa, pylint: disable=too-many-branches + namespace): + """Get arguments for a function call. Args: - args: Arguments to the command. - count: Command repetition count. + func: The function to be called. + count: The count to be added to the call. + namespace: The argparse namespace. + + Return: + An (args, kwargs) tuple. """ - dbgout = ["command called:", self.name] - if args: - dbgout.append(str(args)) - if count is not None: - dbgout.append("(count={})".format(count)) - log.commands.debug(' '.join(dbgout)) - posargs = [] + args = [] kwargs = {} - app = QCoreApplication.instance() - - try: - namespace = self.parser.parse_args(args) - except argparser.ArgumentParserError as e: - message.error('{}: {}'.format(self.name, e)) - return - except argparser.ArgumentParserExit as e: - log.commands.debug("argparser exited with status {}: {}".format( - e.status, e)) - return - - signature = inspect.signature(self.handler) + signature = inspect.signature(func) for i, param in enumerate(signature.parameters.values()): if i == 0 and self.instance is not None: # Special case for 'self'. assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + app = QCoreApplication.instance() if self.instance == '': obj = app else: obj = utils.dotted_getattr(app, self.instance) - posargs.append(obj) + args.append(obj) continue elif param.name == 'count': # Special case for 'count'. if not self.count: raise TypeError("{}: count argument given with a command " "which does not support count!".format( - self.name)) + self.name)) if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: if count is not None: - posargs.append(count) + args.append(count) else: - posargs.append(param.default) + args.append(param.default) elif param.kind == inspect.Parameter.KEYWORD_ONLY: if count is not None: kwargs['count'] = count @@ -167,16 +153,43 @@ class Command: # want. value = self.type_conv[param.name](value) if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: - posargs.append(value) + args.append(value) elif param.kind == inspect.Parameter.VAR_POSITIONAL: if value is not None: - posargs += value + args += value elif param.kind == inspect.Parameter.KEYWORD_ONLY: kwargs[param.name] = value else: raise TypeError("{}: Invalid parameter type {} for argument " "'{}'!".format( self.name, param.kind, param.name)) + return args, kwargs + + def run(self, args=None, count=None): + """Run the command. + + Note we don't catch CommandError here as it might happen async. + + Args: + args: Arguments to the command. + count: Command repetition count. + """ + dbgout = ["command called:", self.name] + if args: + dbgout.append(str(args)) + if count is not None: + dbgout.append("(count={})".format(count)) + log.commands.debug(' '.join(dbgout)) + try: + namespace = self.parser.parse_args(args) + except argparser.ArgumentParserError as e: + message.error('{}: {}'.format(self.name, e)) + return + except argparser.ArgumentParserExit as e: + log.commands.debug("argparser exited with status {}: {}".format( + e.status, e)) + return + posargs, kwargs = self._get_args(self.handler, count, namespace) self._check_prerequisites() log.commands.debug('Calling {}'.format( debug.format_call(self.handler, posargs, kwargs))) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index e135d52cf..bd11228b3 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -283,7 +283,8 @@ class ConfigManager(QObject): @cmdutils.register(name='get', instance='config', completion=[Completion.section, Completion.option]) - def get_command(self, section, option): + def get_command(self, sectname: {'name': 'section'}, + optname: {'name': 'option'}): """Get the value from a section/option. // @@ -291,16 +292,16 @@ class ConfigManager(QObject): Wrapper for the get-command to output the value in the status bar. Args: - section: The section where the option is in. - option: The name of the option. + sectname: The section where the option is in. + optname: The name of the option. """ try: - val = self.get(section, option, transformed=False) + val = self.get(sectname, optname, transformed=False) except (NoOptionError, NoSectionError) as e: raise cmdexc.CommandError("get: {} - {}".format( e.__class__.__name__, e)) else: - message.info("{} {} = {}".format(section, option, val), + message.info("{} {} = {}".format(sectname, optname, val), immediately=True) @functools.lru_cache() @@ -336,7 +337,8 @@ class ConfigManager(QObject): @cmdutils.register(name='set', instance='config', completion=[Completion.section, Completion.option, Completion.value]) - def set_command(self, section, option, value, temp=False): + def set_command(self, section, # pylint: disable=redefined-outer-name + option, value, temp=False): """Set an option. // diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py index f4588f6b5..217ebcb5c 100644 --- a/qutebrowser/config/keyconfparser.py +++ b/qutebrowser/config/keyconfparser.py @@ -161,12 +161,12 @@ class KeyConfigParser(QObject): raise cmdexc.CommandError("Invalid mode {}!".format(m)) try: sect = self.keybindings[mode] - except KeyError as e: + except KeyError: raise cmdexc.CommandError("Can't find mode section '{}'!".format( sect)) try: del sect[key] - except KeyError as e: + except KeyError: raise cmdexc.CommandError("Can't find binding '{}' in section " "'{}'!".format(key, mode)) else: diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py index 0a341dfb8..772c9376a 100644 --- a/qutebrowser/network/qutescheme.py +++ b/qutebrowser/network/qutescheme.py @@ -136,5 +136,3 @@ HANDLERS = { 'gpl': qute_gpl, 'help': qute_help, } - - From f2e68685d26c5c75cd0afcb3f208df69d266fe35 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Sep 2014 00:03:39 +0200 Subject: [PATCH 69/89] Fix doc generation with overridden argument names. --- qutebrowser/commands/cmdutils.py | 4 ++-- scripts/generate_doc.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 771819cff..8a364849a 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -324,10 +324,10 @@ class register: # pylint: disable=invalid-name short_flag = '-{}'.format(shortname) args.append(long_flag) args.append(short_flag) - self.opt_args[name] = long_flag, short_flag + self.opt_args[param.name] = long_flag, short_flag else: args.append(name) - self.pos_args.append(name) + self.pos_args.append((param.name, name)) kwargs.update(annotation_info.kwargs) return args, kwargs diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index c292e45fd..1ee3f400b 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -162,9 +162,9 @@ def _get_command_doc(name, cmd): if cmd.pos_args: output.append("") output.append("==== positional arguments") - for arg in cmd.pos_args: + for arg, name in cmd.pos_args: try: - output.append("* +'{}'+: {}".format(arg, + output.append("* +'{}'+: {}".format(name, parser.arg_descs[arg])) except KeyError as e: raise KeyError("No description for arg {} of command " From fa9d9b62b18c41b1625e916d0eed1a5686568326 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Sep 2014 00:03:59 +0200 Subject: [PATCH 70/89] config: Use sectname/optname argument names. --- qutebrowser/config/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index bd11228b3..70109b493 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -337,8 +337,8 @@ class ConfigManager(QObject): @cmdutils.register(name='set', instance='config', completion=[Completion.section, Completion.option, Completion.value]) - def set_command(self, section, # pylint: disable=redefined-outer-name - option, value, temp=False): + def set_command(self, sectname: {'name': 'section'}, + optname: {'name': 'option'}, value, temp=False): """Set an option. // @@ -346,14 +346,14 @@ class ConfigManager(QObject): Wrapper for self.set() to output exceptions in the status bar. Args: - section: The section where the option is in. - option: The name of the option. + sectname: The section where the option is in. + optname: The name of the option. value: The value to set. temp: Set value temporarily. """ try: layer = 'temp' if temp else 'conf' - self.set(layer, section, option, value) + self.set(layer, sectname, optname, value) except (NoOptionError, NoSectionError, configtypes.ValidationError, ValueError) as e: raise cmdexc.CommandError("set: {} - {}".format( From 22e6a26ec339fbc490e33cbb8a2034d6ae6aecc9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Sep 2014 06:20:33 +0200 Subject: [PATCH 71/89] Fix function calls with changed attribute names. --- qutebrowser/commands/cmdutils.py | 22 +++++++++++++++++++--- qutebrowser/commands/command.py | 9 ++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 8a364849a..fde3d91f7 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -179,14 +179,15 @@ class register: # pylint: disable=invalid-name default=argparser.SUPPRESS, nargs=0, help=argparser.SUPPRESS) self._check_func() - has_count, desc, type_conv = self._inspect_func() + has_count, desc, type_conv, name_conv = self._inspect_func() cmd = command.Command( name=names[0], split=self.split, hide=self.hide, count=has_count, desc=desc, instance=self.instance, handler=func, completion=self.completion, modes=self.modes, not_modes=self.not_modes, needs_js=self.needs_js, is_debug=self.debug, parser=self.parser, type_conv=type_conv, - opt_args=self.opt_args, pos_args=self.pos_args) + opt_args=self.opt_args, pos_args=self.pos_args, + name_conv=name_conv) for name in names: cmd_dict[name] = cmd aliases += names[1:] @@ -240,6 +241,18 @@ class register: # pylint: disable=invalid-name type_conv[param.name] = argparser.multitype_conv(typ) return type_conv + def _get_nameconv(self, param, annotation_info): + """Get a dict with a name conversion for the paraeter. + + Args: + param: The inspect.Parameter to handle. + annotation_info: The AnnotationInfo tuple for the parameter. + """ + d = {} + if annotation_info.name is not None: + d[param.name] = annotation_info.name + return d + def _inspect_func(self): """Inspect the function to get useful informations from it. @@ -248,8 +261,10 @@ class register: # pylint: disable=invalid-name has_count: Whether the command supports a count. desc: The description of the command. type_conv: A mapping of args to type converter callables. + name_conv: A mapping of names to convert. """ type_conv = {} + name_conv = {} signature = inspect.signature(self.func) has_count = 'count' in signature.parameters doc = inspect.getdoc(self.func) @@ -266,13 +281,14 @@ class register: # pylint: disable=invalid-name args, kwargs = self._param_to_argparse_args( param, annotation_info) type_conv.update(self._get_typeconv(param, typ)) + name_conv.update(self._get_nameconv(param, annotation_info)) callsig = debugutils.format_call( self.parser.add_argument, args, kwargs, full=False) log.commands.vdebug('Adding arg {} of type {} -> {}'.format( param.name, typ, callsig)) self.parser.add_argument(*args, **kwargs) - return has_count, desc, type_conv + return has_count, desc, type_conv, name_conv def _param_to_argparse_args(self, param, annotation_info): """Get argparse arguments for a parameter. diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index e3f08f25f..57b13a84f 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -46,6 +46,7 @@ class Command: debug: Whether this is a debugging command (only shown with --debug). parser: The ArgumentParser to use to parse this command. type_conv: A mapping of conversion functions for arguments. + name_conv: A mapping of argument names to parameter names. """ # TODO: @@ -54,7 +55,7 @@ class Command: def __init__(self, name, split, hide, count, desc, instance, handler, completion, modes, not_modes, needs_js, is_debug, parser, - type_conv, opt_args, pos_args): + type_conv, opt_args, pos_args, name_conv): # I really don't know how to solve this in a better way, I tried. # pylint: disable=too-many-arguments,too-many-locals self.name = name @@ -73,6 +74,7 @@ class Command: self.type_conv = type_conv self.opt_args = opt_args self.pos_args = pos_args + self.name_conv = name_conv def _check_prerequisites(self): """Check if the command is permitted to run currently. @@ -145,7 +147,8 @@ class Command: "argument 'count'!".format( self.name, param.kind)) continue - value = getattr(namespace, param.name) + name = self.name_conv.get(param.name, param.name) + value = getattr(namespace, name) if param.name in self.type_conv: # We convert enum types after getting the values from # argparse, because argparse's choices argument is @@ -158,7 +161,7 @@ class Command: if value is not None: args += value elif param.kind == inspect.Parameter.KEYWORD_ONLY: - kwargs[param.name] = value + kwargs[name] = value else: raise TypeError("{}: Invalid parameter type {} for argument " "'{}'!".format( From f7c0f8f11e442f92574a688254d40dc4d7227ca0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Sep 2014 06:24:15 +0200 Subject: [PATCH 72/89] Remove :get and use :set with ...? instead. --- doc/help/commands.asciidoc | 15 +++----------- qutebrowser/config/config.py | 40 +++++++++++++----------------------- 2 files changed, 17 insertions(+), 38 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index ec42906ed..32ebbc5ca 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -10,7 +10,6 @@ |<>|Cancel the first/[count]th download. |<>|Download the current page. |<>|Go forward in the history of the current tab. -|<>|Get the value from a section/option. |<>|Show help about a command or setting. |<>|Start hinting. |<>|Open main startpage in current tab. @@ -86,16 +85,6 @@ Go forward in the history of the current tab. ==== count How many pages to go forward. -[[get]] -=== get -Syntax: +:get 'section' 'option'+ - -Get the value from a section/option. - -==== positional arguments -* +'section'+: The section where the option is in. -* +'option'+: The name of the option. - [[help]] === help Syntax: +:help ['topic']+ @@ -293,10 +282,12 @@ Save the config file. [[set]] === set -Syntax: +:set [*--temp*] 'section' 'option' 'value'+ +Syntax: +:set [*--temp*] 'section' 'option' ['value']+ Set an option. +If the option name ends with '?', the value of the option is shown instead. + ==== positional arguments * +'section'+: The section where the option is in. * +'option'+: The name of the option. diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 70109b493..2aac3ef63 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -281,29 +281,6 @@ class ConfigManager(QObject): self.get.cache_clear() return existed - @cmdutils.register(name='get', instance='config', - completion=[Completion.section, Completion.option]) - def get_command(self, sectname: {'name': 'section'}, - optname: {'name': 'option'}): - """Get the value from a section/option. - - // - - Wrapper for the get-command to output the value in the status bar. - - Args: - sectname: The section where the option is in. - optname: The name of the option. - """ - try: - val = self.get(sectname, optname, transformed=False) - except (NoOptionError, NoSectionError) as e: - raise cmdexc.CommandError("get: {} - {}".format( - e.__class__.__name__, e)) - else: - message.info("{} {} = {}".format(sectname, optname, val), - immediately=True) - @functools.lru_cache() def get(self, sectname, optname, raw=False, transformed=True): """Get the value from a section/option. @@ -338,9 +315,12 @@ class ConfigManager(QObject): completion=[Completion.section, Completion.option, Completion.value]) def set_command(self, sectname: {'name': 'section'}, - optname: {'name': 'option'}, value, temp=False): + optname: {'name': 'option'}, value=None, temp=False): """Set an option. + If the option name ends with '?', the value of the option is shown + instead. + // Wrapper for self.set() to output exceptions in the status bar. @@ -352,8 +332,16 @@ class ConfigManager(QObject): temp: Set value temporarily. """ try: - layer = 'temp' if temp else 'conf' - self.set(layer, sectname, optname, value) + if optname.endswith('?'): + val = self.get(sectname, optname[:-1], transformed=False) + message.info("{} {} = {}".format(sectname, optname[:-1], val), + immediately=True) + else: + if value is None: + raise cmdexc.CommandError("set: The following arguments " + "are required: value") + layer = 'temp' if temp else 'conf' + self.set(layer, sectname, optname, value) except (NoOptionError, NoSectionError, configtypes.ValidationError, ValueError) as e: raise cmdexc.CommandError("set: {} - {}".format( From e1d93fa3fa2ddb66ab85488f1a2b5c2db3658ed7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Sep 2014 07:42:21 +0200 Subject: [PATCH 73/89] Move inspect stuff from cmdutils to command. --- qutebrowser/commands/cmdutils.py | 260 ++++--------------------------- qutebrowser/commands/command.py | 223 ++++++++++++++++++++++++-- scripts/generate_doc.py | 2 +- 3 files changed, 236 insertions(+), 249 deletions(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index fde3d91f7..d7707d26e 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -23,12 +23,8 @@ Module attributes: cmd_dict: A mapping from command-strings to command objects. """ -import inspect -import collections - -from qutebrowser.utils import usertypes, qtutils, log, utils -from qutebrowser.utils import debug as debugutils -from qutebrowser.commands import command, cmdexc, argparser +from qutebrowser.utils import usertypes, qtutils, log +from qutebrowser.commands import command, cmdexc cmd_dict = {} aliases = [] @@ -105,14 +101,8 @@ class register: # pylint: disable=invalid-name needs_js: If javascript is needed for this command. debug: Whether this is a debugging command (only shown with --debug). ignore_args: Whether to ignore the arguments of the function. - - Class attributes: - AnnotationInfo: Named tuple for info from an annotation. """ - AnnotationInfo = collections.namedtuple('AnnotationInfo', - 'kwargs, typ, name, flag') - def __init__(self, instance=None, name=None, split=True, hide=False, completion=None, modes=None, not_modes=None, needs_js=False, debug=False, ignore_args=False): @@ -136,11 +126,6 @@ class register: # pylint: disable=invalid-name self.needs_js = needs_js self.debug = debug self.ignore_args = ignore_args - self.parser = None - self.func = None - self.docparser = None - self.opt_args = collections.OrderedDict() - self.pos_args = [] if modes is not None: for m in modes: if not isinstance(m, usertypes.KeyMode): @@ -150,6 +135,28 @@ class register: # pylint: disable=invalid-name if not isinstance(m, usertypes.KeyMode): raise TypeError("Mode {} is no KeyMode member!".format(m)) + def _get_names(self, func): + """Get the name(s) which should be used for the current command. + + If the name hasn't been overridden explicitely, the function name is + transformed. + + If it has been set, it can either be a string which is + used directly, or an iterable. + + Args: + func: The function to get the name of. + + Return: + A list of names, with the main name being the first item. + """ + if self.name is None: + return [func.__name__.lower().replace('_', '-')] + elif isinstance(self.name, str): + return [self.name] + else: + return self.name + def __call__(self, func): """Register the command before running the function. @@ -165,226 +172,17 @@ class register: # pylint: disable=invalid-name The original function (unmodified). """ global aliases - self.func = func - names = self._get_names() + names = self._get_names(func) log.commands.vdebug("Registering command {}".format(names[0])) for name in names: if name in cmd_dict: raise ValueError("{} is already registered!".format(name)) - self.docparser = utils.DocstringParser(func) - self.parser = argparser.ArgumentParser( - names[0], description=self.docparser.short_desc, - epilog=self.docparser.long_desc) - self.parser.add_argument('-h', '--help', action=argparser.HelpAction, - default=argparser.SUPPRESS, nargs=0, - help=argparser.SUPPRESS) - self._check_func() - has_count, desc, type_conv, name_conv = self._inspect_func() cmd = command.Command( - name=names[0], split=self.split, hide=self.hide, count=has_count, - desc=desc, instance=self.instance, handler=func, - completion=self.completion, modes=self.modes, - not_modes=self.not_modes, needs_js=self.needs_js, - is_debug=self.debug, parser=self.parser, type_conv=type_conv, - opt_args=self.opt_args, pos_args=self.pos_args, - name_conv=name_conv) + name=names[0], split=self.split, hide=self.hide, + instance=self.instance, completion=self.completion, + modes=self.modes, not_modes=self.not_modes, needs_js=self.needs_js, + is_debug=self.debug, ignore_args=self.ignore_args, handler=func) for name in names: cmd_dict[name] = cmd aliases += names[1:] return func - - def _get_names(self): - """Get the name(s) which should be used for the current command. - - If the name hasn't been overridden explicitely, the function name is - transformed. - - If it has been set, it can either be a string which is - used directly, or an iterable. - - Return: - A list of names, with the main name being the first item. - """ - if self.name is None: - return [self.func.__name__.lower().replace('_', '-')] - elif isinstance(self.name, str): - return [self.name] - else: - return self.name - - def _check_func(self): - """Make sure the function parameters don't violate any rules.""" - signature = inspect.signature(self.func) - if 'self' in signature.parameters and self.instance is None: - raise TypeError("{} is a class method, but instance was not " - "given!".format(self.name[0])) - elif 'self' not in signature.parameters and self.instance is not None: - raise TypeError("{} is not a class method, but instance was " - "given!".format(self.name[0])) - elif inspect.getfullargspec(self.func).varkw is not None: - raise TypeError("{}: functions with varkw arguments are not " - "supported!".format(self.name[0])) - - def _get_typeconv(self, param, typ): - """Get a dict with a type conversion for the parameter. - - Args: - param: The inspect.Parameter to handle. - typ: The type of the parameter. - """ - type_conv = {} - if utils.is_enum(typ): - type_conv[param.name] = argparser.enum_getter(typ) - elif isinstance(typ, tuple): - if param.default is not inspect.Parameter.empty: - typ = typ + (type(param.default),) - type_conv[param.name] = argparser.multitype_conv(typ) - return type_conv - - def _get_nameconv(self, param, annotation_info): - """Get a dict with a name conversion for the paraeter. - - Args: - param: The inspect.Parameter to handle. - annotation_info: The AnnotationInfo tuple for the parameter. - """ - d = {} - if annotation_info.name is not None: - d[param.name] = annotation_info.name - return d - - def _inspect_func(self): - """Inspect the function to get useful informations from it. - - Return: - A (has_count, desc, parser, type_conv) tuple. - has_count: Whether the command supports a count. - desc: The description of the command. - type_conv: A mapping of args to type converter callables. - name_conv: A mapping of names to convert. - """ - type_conv = {} - name_conv = {} - signature = inspect.signature(self.func) - has_count = 'count' in signature.parameters - doc = inspect.getdoc(self.func) - if doc is not None: - desc = doc.splitlines()[0].strip() - else: - desc = "" - if not self.ignore_args: - for param in signature.parameters.values(): - if param.name in ('self', 'count'): - continue - annotation_info = self._parse_annotation(param) - typ = self._get_type(param, annotation_info) - args, kwargs = self._param_to_argparse_args( - param, annotation_info) - type_conv.update(self._get_typeconv(param, typ)) - name_conv.update(self._get_nameconv(param, annotation_info)) - callsig = debugutils.format_call( - self.parser.add_argument, args, kwargs, - full=False) - log.commands.vdebug('Adding arg {} of type {} -> {}'.format( - param.name, typ, callsig)) - self.parser.add_argument(*args, **kwargs) - return has_count, desc, type_conv, name_conv - - def _param_to_argparse_args(self, param, annotation_info): - """Get argparse arguments for a parameter. - - Return: - An (args, kwargs) tuple. - - Args: - param: The inspect.Parameter object to get the args for. - annotation_info: An AnnotationInfo tuple for the parameter. - """ - - ParamType = usertypes.enum('ParamType', 'flag', 'positional') - - kwargs = {} - typ = self._get_type(param, annotation_info) - param_type = ParamType.positional - - try: - kwargs['help'] = self.docparser.arg_descs[param.name] - except KeyError: - pass - - if isinstance(typ, tuple): - pass - elif utils.is_enum(typ): - kwargs['choices'] = [e.name.replace('_', '-') for e in typ] - kwargs['metavar'] = param.name - elif typ is bool: - param_type = ParamType.flag - kwargs['action'] = 'store_true' - elif typ is not None: - kwargs['type'] = typ - - if param.kind == inspect.Parameter.VAR_POSITIONAL: - kwargs['nargs'] = '+' - elif param.kind == inspect.Parameter.KEYWORD_ONLY: - param_type = ParamType.flag - kwargs['default'] = param.default - elif typ is not bool and param.default is not inspect.Parameter.empty: - kwargs['default'] = param.default - kwargs['nargs'] = '?' - - args = [] - name = annotation_info.name or param.name - shortname = annotation_info.flag or param.name[0] - if param_type == ParamType.flag: - long_flag = '--{}'.format(name) - short_flag = '-{}'.format(shortname) - args.append(long_flag) - args.append(short_flag) - self.opt_args[param.name] = long_flag, short_flag - else: - args.append(name) - self.pos_args.append((param.name, name)) - kwargs.update(annotation_info.kwargs) - return args, kwargs - - def _parse_annotation(self, param): - """Get argparse arguments and type from a parameter annotation. - - Args: - param: A inspect.Parameter instance. - - Return: - An AnnotationInfo namedtuple. - kwargs: A dict of keyword args to add to the - argparse.ArgumentParser.add_argument call. - typ: The type to use for this argument. - flag: The short name/flag if overridden. - name: The long name if overridden. - """ - info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None} - if param.annotation is not inspect.Parameter.empty: - log.commands.vdebug("Parsing annotation {}".format( - param.annotation)) - if isinstance(param.annotation, dict): - for field in ('type', 'flag', 'name'): - if field in param.annotation: - info[field] = param.annotation[field] - del param.annotation[field] - info['kwargs'] = param.annotation - else: - info['typ'] = param.annotation - return self.AnnotationInfo(**info) - - def _get_type(self, param, annotation_info): - """Get the type of an argument from its default value or annotation. - - Args: - param: The inspect.Parameter to look at. - annotation_info: An AnnotationInfo tuple which overrides the type. - """ - if annotation_info.typ is not None: - return annotation_info.typ - elif param.default is None or param.default is inspect.Parameter.empty: - return None - else: - return type(param.default) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 57b13a84f..c12c7d1d5 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -20,12 +20,13 @@ """Contains the Command class, a skeleton for a command.""" import inspect +import collections from PyQt5.QtCore import QCoreApplication from PyQt5.QtWebKit import QWebSettings from qutebrowser.commands import cmdexc, argparser -from qutebrowser.utils import log, utils, message, debug +from qutebrowser.utils import log, utils, message, debug, usertypes class Command: @@ -47,33 +48,44 @@ class Command: parser: The ArgumentParser to use to parse this command. type_conv: A mapping of conversion functions for arguments. name_conv: A mapping of argument names to parameter names. + + Class attributes: + AnnotationInfo: Named tuple for info from an annotation. """ - # TODO: - # we should probably have some kind of typing / argument casting for args - # this might be combined with help texts or so as well + AnnotationInfo = collections.namedtuple('AnnotationInfo', + 'kwargs, typ, name, flag') - def __init__(self, name, split, hide, count, desc, instance, handler, - completion, modes, not_modes, needs_js, is_debug, parser, - type_conv, opt_args, pos_args, name_conv): + def __init__(self, name, split, hide, instance, completion, modes, + not_modes, needs_js, is_debug, ignore_args, + handler): # I really don't know how to solve this in a better way, I tried. # pylint: disable=too-many-arguments,too-many-locals self.name = name self.split = split self.hide = hide - self.count = count - self.desc = desc self.instance = instance - self.handler = handler self.completion = completion self.modes = modes self.not_modes = not_modes self.needs_js = needs_js self.debug = is_debug - self.parser = parser + self.ignore_args = ignore_args + self.handler = handler + self.docparser = utils.DocstringParser(handler) + self.parser = argparser.ArgumentParser( + name, description=self.docparser.short_desc, + epilog=self.docparser.long_desc) + self.parser.add_argument('-h', '--help', action=argparser.HelpAction, + default=argparser.SUPPRESS, nargs=0, + help=argparser.SUPPRESS) + self._check_func() + self.opt_args = collections.OrderedDict() + self.pos_args = [] + has_count, desc, type_conv, name_conv = self._inspect_func() + self.has_count = has_count + self.desc = desc self.type_conv = type_conv - self.opt_args = opt_args - self.pos_args = pos_args self.name_conv = name_conv def _check_prerequisites(self): @@ -100,8 +112,185 @@ class Command: raise cmdexc.PrerequisitesError( "{}: This command needs javascript enabled.".format(self.name)) - def _get_args(self, func, count, # noqa, pylint: disable=too-many-branches - namespace): + def _check_func(self): + """Make sure the function parameters don't violate any rules.""" + signature = inspect.signature(self.handler) + if 'self' in signature.parameters and self.instance is None: + raise TypeError("{} is a class method, but instance was not " + "given!".format(self.name[0])) + elif 'self' not in signature.parameters and self.instance is not None: + raise TypeError("{} is not a class method, but instance was " + "given!".format(self.name[0])) + elif inspect.getfullargspec(self.handler).varkw is not None: + raise TypeError("{}: functions with varkw arguments are not " + "supported!".format(self.name[0])) + + def _get_typeconv(self, param, typ): + """Get a dict with a type conversion for the parameter. + + Args: + param: The inspect.Parameter to handle. + typ: The type of the parameter. + """ + type_conv = {} + if utils.is_enum(typ): + type_conv[param.name] = argparser.enum_getter(typ) + elif isinstance(typ, tuple): + if param.default is not inspect.Parameter.empty: + typ = typ + (type(param.default),) + type_conv[param.name] = argparser.multitype_conv(typ) + return type_conv + + def _get_nameconv(self, param, annotation_info): + """Get a dict with a name conversion for the paraeter. + + Args: + param: The inspect.Parameter to handle. + annotation_info: The AnnotationInfo tuple for the parameter. + """ + d = {} + if annotation_info.name is not None: + d[param.name] = annotation_info.name + return d + + def _inspect_func(self): + """Inspect the function to get useful informations from it. + + Return: + A (has_count, desc, parser, type_conv) tuple. + has_count: Whether the command supports a count. + desc: The description of the command. + type_conv: A mapping of args to type converter callables. + name_conv: A mapping of names to convert. + """ + type_conv = {} + name_conv = {} + signature = inspect.signature(self.handler) + has_count = 'count' in signature.parameters + doc = inspect.getdoc(self.handler) + if doc is not None: + desc = doc.splitlines()[0].strip() + else: + desc = "" + if not self.ignore_args: + for param in signature.parameters.values(): + if param.name in ('self', 'count'): + continue + annotation_info = self._parse_annotation(param) + typ = self._get_type(param, annotation_info) + args, kwargs = self._param_to_argparse_args( + param, annotation_info) + type_conv.update(self._get_typeconv(param, typ)) + name_conv.update(self._get_nameconv(param, annotation_info)) + callsig = debug.format_call( + self.parser.add_argument, args, kwargs, + full=False) + log.commands.vdebug('Adding arg {} of type {} -> {}'.format( + param.name, typ, callsig)) + self.parser.add_argument(*args, **kwargs) + return has_count, desc, type_conv, name_conv + + def _param_to_argparse_args(self, param, annotation_info): + """Get argparse arguments for a parameter. + + Return: + An (args, kwargs) tuple. + + Args: + param: The inspect.Parameter object to get the args for. + annotation_info: An AnnotationInfo tuple for the parameter. + """ + + ParamType = usertypes.enum('ParamType', 'flag', 'positional') + + kwargs = {} + typ = self._get_type(param, annotation_info) + param_type = ParamType.positional + + try: + kwargs['help'] = self.docparser.arg_descs[param.name] + except KeyError: + pass + + if isinstance(typ, tuple): + pass + elif utils.is_enum(typ): + kwargs['choices'] = [e.name.replace('_', '-') for e in typ] + kwargs['metavar'] = param.name + elif typ is bool: + param_type = ParamType.flag + kwargs['action'] = 'store_true' + elif typ is not None: + kwargs['type'] = typ + + if param.kind == inspect.Parameter.VAR_POSITIONAL: + kwargs['nargs'] = '+' + elif param.kind == inspect.Parameter.KEYWORD_ONLY: + param_type = ParamType.flag + kwargs['default'] = param.default + elif typ is not bool and param.default is not inspect.Parameter.empty: + kwargs['default'] = param.default + kwargs['nargs'] = '?' + + args = [] + name = annotation_info.name or param.name + shortname = annotation_info.flag or param.name[0] + if param_type == ParamType.flag: + long_flag = '--{}'.format(name) + short_flag = '-{}'.format(shortname) + args.append(long_flag) + args.append(short_flag) + self.opt_args[param.name] = long_flag, short_flag + else: + args.append(name) + self.pos_args.append((param.name, name)) + kwargs.update(annotation_info.kwargs) + return args, kwargs + + def _parse_annotation(self, param): + """Get argparse arguments and type from a parameter annotation. + + Args: + param: A inspect.Parameter instance. + + Return: + An AnnotationInfo namedtuple. + kwargs: A dict of keyword args to add to the + argparse.ArgumentParser.add_argument call. + typ: The type to use for this argument. + flag: The short name/flag if overridden. + name: The long name if overridden. + """ + info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None} + if param.annotation is not inspect.Parameter.empty: + log.commands.vdebug("Parsing annotation {}".format( + param.annotation)) + if isinstance(param.annotation, dict): + for field in ('type', 'flag', 'name'): + if field in param.annotation: + info[field] = param.annotation[field] + del param.annotation[field] + info['kwargs'] = param.annotation + else: + info['typ'] = param.annotation + return self.AnnotationInfo(**info) + + def _get_type(self, param, annotation_info): + """Get the type of an argument from its default value or annotation. + + Args: + param: The inspect.Parameter to look at. + annotation_info: An AnnotationInfo tuple which overrides the type. + """ + if annotation_info.typ is not None: + return annotation_info.typ + elif param.default is None or param.default is inspect.Parameter.empty: + return None + else: + return type(param.default) + + def _get_call_args(self, func, # noqa, pylint: disable=too-many-branches + count, namespace): """Get arguments for a function call. Args: @@ -130,7 +319,7 @@ class Command: continue elif param.name == 'count': # Special case for 'count'. - if not self.count: + if not self.has_count: raise TypeError("{}: count argument given with a command " "which does not support count!".format( self.name)) @@ -192,7 +381,7 @@ class Command: log.commands.debug("argparser exited with status {}: {}".format( e.status, e)) return - posargs, kwargs = self._get_args(self.handler, count, namespace) + posargs, kwargs = self._get_call_args(self.handler, count, namespace) self._check_prerequisites() log.commands.debug('Calling {}'.format( debug.format_call(self.handler, posargs, kwargs))) diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index 1ee3f400b..f247f7071 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -181,7 +181,7 @@ def _get_command_doc(name, cmd): raise KeyError("No description for arg {} of command " "'{}'!".format(e, cmd.name)) - if cmd.count: + if cmd.has_count: output.append("") output.append("==== count") output.append(parser.arg_descs['count']) From 8a51aa759e868dda3c206524b04b4999103f8e60 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Sep 2014 07:43:40 +0200 Subject: [PATCH 74/89] command: Clean up ParamType. --- qutebrowser/commands/command.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index c12c7d1d5..3a7300be5 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -51,10 +51,12 @@ class Command: Class attributes: AnnotationInfo: Named tuple for info from an annotation. + ParamType: Enum for an argparse parameter type. """ AnnotationInfo = collections.namedtuple('AnnotationInfo', 'kwargs, typ, name, flag') + ParamType = usertypes.enum('ParamType', 'flag', 'positional') def __init__(self, name, split, hide, instance, completion, modes, not_modes, needs_js, is_debug, ignore_args, @@ -201,11 +203,9 @@ class Command: annotation_info: An AnnotationInfo tuple for the parameter. """ - ParamType = usertypes.enum('ParamType', 'flag', 'positional') - kwargs = {} typ = self._get_type(param, annotation_info) - param_type = ParamType.positional + param_type = self.ParamType.positional try: kwargs['help'] = self.docparser.arg_descs[param.name] @@ -218,7 +218,7 @@ class Command: kwargs['choices'] = [e.name.replace('_', '-') for e in typ] kwargs['metavar'] = param.name elif typ is bool: - param_type = ParamType.flag + param_type = self.ParamType.flag kwargs['action'] = 'store_true' elif typ is not None: kwargs['type'] = typ @@ -226,7 +226,7 @@ class Command: if param.kind == inspect.Parameter.VAR_POSITIONAL: kwargs['nargs'] = '+' elif param.kind == inspect.Parameter.KEYWORD_ONLY: - param_type = ParamType.flag + param_type = self.ParamType.flag kwargs['default'] = param.default elif typ is not bool and param.default is not inspect.Parameter.empty: kwargs['default'] = param.default @@ -235,15 +235,17 @@ class Command: args = [] name = annotation_info.name or param.name shortname = annotation_info.flag or param.name[0] - if param_type == ParamType.flag: + if param_type == self.ParamType.flag: long_flag = '--{}'.format(name) short_flag = '-{}'.format(shortname) args.append(long_flag) args.append(short_flag) self.opt_args[param.name] = long_flag, short_flag - else: + elif param_type == self.ParamType.positional: args.append(name) self.pos_args.append((param.name, name)) + else: + raise ValueError("Invalid ParamType {}!".format(param_type)) kwargs.update(annotation_info.kwargs) return args, kwargs From 34b0cf429cfba93ad4ca652e684b72ef7b7558ca Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Sep 2014 08:16:19 +0200 Subject: [PATCH 75/89] command: Cleanup --- qutebrowser/commands/command.py | 101 +++++++++++++++++++------------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 3a7300be5..af34eb825 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -83,6 +83,8 @@ class Command: help=argparser.SUPPRESS) self._check_func() self.opt_args = collections.OrderedDict() + self.namespace = None + self.count = None self.pos_args = [] has_count, desc, type_conv, name_conv = self._inspect_func() self.has_count = has_count @@ -291,14 +293,58 @@ class Command: else: return type(param.default) - def _get_call_args(self, func, # noqa, pylint: disable=too-many-branches - count, namespace): - """Get arguments for a function call. + def _get_self_arg(self, param, args): + """Get the self argument for a function call. - Args: - func: The function to be called. - count: The count to be added to the call. - namespace: The argparse namespace. + Arguments: + param: The count parameter. + args: The positional argument list. Gets modified directly. + """ + assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + app = QCoreApplication.instance() + if self.instance == '': + obj = app + else: + obj = utils.dotted_getattr(app, self.instance) + args.append(obj) + + def _get_count_arg(self, param, args, kwargs): + """Add the count argument to a function call. + + Arguments: + param: The count parameter. + args: The positional argument list. Gets modified directly. + kwargs: The keyword argument dict. Gets modified directly. + """ + if not self.has_count: + raise TypeError("{}: count argument given with a command which " + "does not support count!".format(self.name)) + if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: + if self.count is not None: + args.append(self.count) + else: + args.append(param.default) + elif param.kind == inspect.Parameter.KEYWORD_ONLY: + if self.count is not None: + kwargs['count'] = self.count + else: + raise TypeError("{}: invalid parameter type {} for argument " + "'count'!".format(self.name, param.kind)) + + def _get_param_name_and_value(self, param): + """Get the converted name and value for an inspect.Parameter.""" + name = self.name_conv.get(param.name, param.name) + value = getattr(self.namespace, name) + if param.name in self.type_conv: + # We convert enum types after getting the values from + # argparse, because argparse's choices argument is + # processed after type conversation, which is not what we + # want. + value = self.type_conv[param.name](value) + return name, value + + def _get_call_args(self): + """Get arguments for a function call. Return: An (args, kwargs) tuple. @@ -306,46 +352,18 @@ class Command: args = [] kwargs = {} - signature = inspect.signature(func) + signature = inspect.signature(self.handler) for i, param in enumerate(signature.parameters.values()): if i == 0 and self.instance is not None: # Special case for 'self'. - assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD - app = QCoreApplication.instance() - if self.instance == '': - obj = app - else: - obj = utils.dotted_getattr(app, self.instance) - args.append(obj) + self._get_self_arg(param, args) continue elif param.name == 'count': # Special case for 'count'. - if not self.has_count: - raise TypeError("{}: count argument given with a command " - "which does not support count!".format( - self.name)) - if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: - if count is not None: - args.append(count) - else: - args.append(param.default) - elif param.kind == inspect.Parameter.KEYWORD_ONLY: - if count is not None: - kwargs['count'] = count - else: - raise TypeError("{}: invalid parameter type {} for " - "argument 'count'!".format( - self.name, param.kind)) + self._get_count_arg(param, args, kwargs) continue - name = self.name_conv.get(param.name, param.name) - value = getattr(namespace, name) - if param.name in self.type_conv: - # We convert enum types after getting the values from - # argparse, because argparse's choices argument is - # processed after type conversation, which is not what we - # want. - value = self.type_conv[param.name](value) + name, value = self._get_param_name_and_value(param) if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: args.append(value) elif param.kind == inspect.Parameter.VAR_POSITIONAL: @@ -375,7 +393,7 @@ class Command: dbgout.append("(count={})".format(count)) log.commands.debug(' '.join(dbgout)) try: - namespace = self.parser.parse_args(args) + self.namespace = self.parser.parse_args(args) except argparser.ArgumentParserError as e: message.error('{}: {}'.format(self.name, e)) return @@ -383,7 +401,8 @@ class Command: log.commands.debug("argparser exited with status {}: {}".format( e.status, e)) return - posargs, kwargs = self._get_call_args(self.handler, count, namespace) + self.count = count + posargs, kwargs = self._get_call_args() self._check_prerequisites() log.commands.debug('Calling {}'.format( debug.format_call(self.handler, posargs, kwargs))) From 3bfc555075d17746b7fafe6229e504099250b180 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Sep 2014 20:08:08 +0200 Subject: [PATCH 76/89] Split generate_doc.py --- scripts/asciidoc2html.py | 73 ++++++++++++++++++++++++++++++++++++++++ scripts/generate_doc.py | 44 +++--------------------- 2 files changed, 77 insertions(+), 40 deletions(-) create mode 100644 scripts/asciidoc2html.py diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py new file mode 100644 index 000000000..9fc5c78fe --- /dev/null +++ b/scripts/asciidoc2html.py @@ -0,0 +1,73 @@ +#!/usr/bin/python3 +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014 Florian Bruhin (The Compiler) + +# 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 . + +"""Generate the html documentation based on the asciidoc files.""" + +import os +import subprocess +import glob + +import colorama as col + + +def call_asciidoc(src, dst): + """Call asciidoc for the given files. + + Args: + src: The source .asciidoc file. + dst: The destination .html file, or None to auto-guess. + """ + print("{}Calling asciidoc for {}...{}".format( + col.Fore.CYAN, os.path.basename(src), col.Fore.RESET)) + if os.name == 'nt': + # FIXME this is highly specific to my machine + args = [r'C:\Python27\python', r'J:\bin\asciidoc-8.6.9\asciidoc.py'] + else: + args = ['asciidoc'] + if dst is not None: + args += ['--out-file', dst] + args.append(src) + try: + subprocess.check_call(args) + except subprocess.CalledProcessError as e: + print(''.join([col.Fore.RED, str(e), col.Fore.RESET])) + sys.exit(1) + + +def main(): + asciidoc_files = [ + ('doc/qutebrowser.1.asciidoc', None), + ('README.asciidoc', None), + ('doc/FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'), + ] + try: + os.mkdir('qutebrowser/html/doc') + except FileExistsError: + pass + for src in glob.glob('doc/help/*.asciidoc'): + name, _ext = os.path.splitext(os.path.basename(src)) + dst = 'qutebrowser/html/doc/{}.html'.format(name) + asciidoc_files.append((src, dst)) + for src, dst in asciidoc_files: + call_asciidoc(src, dst) + + +if __name__ == '__main__': + main() diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py index f247f7071..8d32b136c 100755 --- a/scripts/generate_doc.py +++ b/scripts/generate_doc.py @@ -22,7 +22,6 @@ import os import sys -import glob import html import shutil import os.path @@ -38,6 +37,7 @@ sys.path.insert(0, os.getcwd()) # We import qutebrowser.app so all @cmdutils-register decorators are run. import qutebrowser.app +from scripts import asciidoc2html from qutebrowser import qutebrowser from qutebrowser.commands import cmdutils from qutebrowser.config import configdata @@ -395,30 +395,6 @@ def regenerate_manpage(filename): _format_block(filename, 'options', options) -def call_asciidoc(src, dst): - """Call asciidoc for the given files. - - Args: - src: The source .asciidoc file. - dst: The destination .html file, or None to auto-guess. - """ - print("{}Calling asciidoc for {}...{}".format( - col.Fore.CYAN, os.path.basename(src), col.Fore.RESET)) - if os.name == 'nt': - # FIXME this is highly specific to my machine - args = [r'C:\Python27\python', r'J:\bin\asciidoc-8.6.9\asciidoc.py'] - else: - args = ['asciidoc'] - if dst is not None: - args += ['--out-file', dst] - args.append(src) - try: - subprocess.check_call(args) - except subprocess.CalledProcessError as e: - print(''.join([col.Fore.RED, str(e), col.Fore.RESET])) - sys.exit(1) - - def main(): """Regenerate all documentation.""" print("{}Generating asciidoc files...{}".format( @@ -427,21 +403,9 @@ def main(): generate_settings('doc/help/settings.asciidoc') generate_commands('doc/help/commands.asciidoc') regenerate_authors('README.asciidoc') - asciidoc_files = [ - ('doc/qutebrowser.1.asciidoc', None), - ('README.asciidoc', None), - ('doc/FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'), - ] - try: - os.mkdir('qutebrowser/html/doc') - except FileExistsError: - pass - for src in glob.glob('doc/help/*.asciidoc'): - name, _ext = os.path.splitext(os.path.basename(src)) - dst = 'qutebrowser/html/doc/{}.html'.format(name) - asciidoc_files.append((src, dst)) - for src, dst in asciidoc_files: - call_asciidoc(src, dst) + if '--html' in sys.argv: + asciidoc2html.main() + if __name__ == '__main__': main() From e0c0db68f6e483b4e2802edf84c888467d425c6e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Sep 2014 20:08:34 +0200 Subject: [PATCH 77/89] scripts: Rename generate_doc to src2asciidoc --- scripts/{generate_doc.py => src2asciidoc.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/{generate_doc.py => src2asciidoc.py} (100%) diff --git a/scripts/generate_doc.py b/scripts/src2asciidoc.py similarity index 100% rename from scripts/generate_doc.py rename to scripts/src2asciidoc.py From a1e257ac4ad74690c18da47c2bffcacba5aed49c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Sep 2014 20:12:14 +0200 Subject: [PATCH 78/89] scripts: Add qutebrowser/html/doc to cleanup --- scripts/cleanup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cleanup.py b/scripts/cleanup.py index 2315e061e..0082f8882 100755 --- a/scripts/cleanup.py +++ b/scripts/cleanup.py @@ -32,7 +32,7 @@ recursive_lint = ('__pycache__', '*.pyc') lint = ('build', 'dist', 'pkg/pkg', 'pkg/qutebrowser-*.pkg.tar.xz', 'pkg/src', 'pkg/qutebrowser', 'qutebrowser.egg-info', 'setuptools-*.egg', 'setuptools-*.zip', 'doc/qutebrowser.asciidoc', 'doc/*.html', - 'doc/qutebrowser.1', 'README.html') + 'doc/qutebrowser.1', 'README.html', 'qutebrowser/html/doc') def remove(path): From 0ce54ec1fcef6b85fd89dc7c513cafced4bcc1e8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Sep 2014 22:16:45 +0200 Subject: [PATCH 79/89] Update notes with asciidoc HTML stuff --- doc/notes | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/notes b/doc/notes index 1e0ef2f0d..ea91ea2ec 100644 --- a/doc/notes +++ b/doc/notes @@ -61,3 +61,17 @@ Completion view (not QTreeView) Perhaps using a QHBoxLayout of QTableViews and creating/destroying them based on the completion would be a better idea? + +HTML help pages +=============== + +- Only generate HTML when releasing (and ship it with the releases!) + (setuptools integration) +X Update asciidoc along with source updates +X Provide script to generate HTML from asciidoc +- Show error page with some instructions when HTMLs are missing. +- Show some kind of message when: + - .html files are found + - .asciidoc files are found (because qutebrowser is running locally from + gitrepo) + - .asciidoc files are newer than .html files From c9a24f32f56dc1dc5e1eb29c593b2daac6a83286 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Sep 2014 07:44:04 +0200 Subject: [PATCH 80/89] Use new utils module for colors in asciidoc2html. --- scripts/asciidoc2html.py | 16 +++++---- scripts/utils.py | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 scripts/utils.py diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index 9fc5c78fe..e43173c02 100644 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -21,10 +21,13 @@ """Generate the html documentation based on the asciidoc files.""" import os +import sys import subprocess import glob -import colorama as col +sys.path.insert(0, os.getcwd()) + +from scripts import utils def call_asciidoc(src, dst): @@ -34,8 +37,8 @@ def call_asciidoc(src, dst): src: The source .asciidoc file. dst: The destination .html file, or None to auto-guess. """ - print("{}Calling asciidoc for {}...{}".format( - col.Fore.CYAN, os.path.basename(src), col.Fore.RESET)) + utils.print_col("Calling asciidoc for {}...".format( + os.path.basename(src)), 'cyan') if os.name == 'nt': # FIXME this is highly specific to my machine args = [r'C:\Python27\python', r'J:\bin\asciidoc-8.6.9\asciidoc.py'] @@ -47,11 +50,12 @@ def call_asciidoc(src, dst): try: subprocess.check_call(args) except subprocess.CalledProcessError as e: - print(''.join([col.Fore.RED, str(e), col.Fore.RESET])) + utils.print_col(str(e), 'red') sys.exit(1) -def main(): +def main(colors=False): + utils.use_color = colors asciidoc_files = [ ('doc/qutebrowser.1.asciidoc', None), ('README.asciidoc', None), @@ -70,4 +74,4 @@ def main(): if __name__ == '__main__': - main() + main(colors=True) diff --git a/scripts/utils.py b/scripts/utils.py new file mode 100644 index 000000000..503b7b39d --- /dev/null +++ b/scripts/utils.py @@ -0,0 +1,71 @@ +#!/usr/bin/python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014 Florian Bruhin (The Compiler) + +# 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 . + +"""Utility functions for scripts.""" + + +use_color = True + + +fg_colors = { + 'black': 30, + 'red': 31, + 'green': 32, + 'yellow': 33, + 'blue': 34, + 'magenta': 35, + 'cyan': 36, + 'white': 37, + 'reset': 39, +} + + +bg_colors = {name: col + 10 for name, col in fg_colors.items()} + + +term_attributes = { + 'bright': 1, + 'dim': 2, + 'normal': 22, + 'reset': 0, +} + + +def _esc(code): + return '\033[{}m'.format(code) + + +def print_col(text, color): + """Print a colorized text.""" + if use_color: + fg = _esc(fg_colors[color.lower()]) + reset = _esc(fg_colors['reset']) + print(''.join([fg, text, reset])) + else: + print(text) + + +def print_bold(text): + """Print a bold text.""" + if use_color: + bold = _esc(term_attributes['bright']) + reset = _esc(term_attributes['reset']) + print(''.join([bold, text, reset])) + else: + print(text) From 81b6a921836bfbbcf8f19fea055c3bf374e3aecf Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 21 Sep 2014 21:21:41 +0200 Subject: [PATCH 81/89] Add error if help was not found --- qutebrowser/network/qutescheme.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py index 772c9376a..cb9e9ae6d 100644 --- a/qutebrowser/network/qutescheme.py +++ b/qutebrowser/network/qutescheme.py @@ -121,6 +121,19 @@ def qute_gpl(_request): def qute_help(request): """Handler for qute:help. Return HTML content as bytes.""" + try: + utils.read_file('html/doc/index.html') + except FileNotFoundError: + html = jinja.env.get_template('error.html').render( + title="Error while loading documentation", + url=request.url().toDisplayString(), + error="This most likely means the documentation was not generated " + "properly. If you are running qutebrowser from the git " + "repository, please run scripts/asciidoc2html.py." + "If you're running a released version this is a bug, please " + "use :report to report it.", + icon='') + return html.encode('UTF-8', errors='xmlcharrefreplace') urlpath = request.url().path() if not urlpath or urlpath == '/': urlpath = 'index.html' From ce6778f1d52165c6a1a5e5b7978e20d4bb9bccf0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 21 Sep 2014 21:23:16 +0200 Subject: [PATCH 82/89] doc: Fix settings anchors --- doc/help/settings.asciidoc | 274 ++++++++++++++++++------------------- scripts/src2asciidoc.py | 2 +- 2 files changed, 138 insertions(+), 138 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 8401a16bc..fd6313843 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -4,203 +4,203 @@ [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description -|<>|Whether to find text on a page case-insensitively. -|<>|Whether to wrap finding text to the top when arriving at the end. -|<>|The default page(s) to open at the start, separated by commas. -|<>|Whether to start a search when something else than a URL is entered. -|<>|Whether to save the config automatically on quit. -|<>|The editor (and arguments) to use for the `open-editor` command. -|<>|Encoding to use for editor. -|<>|Do not record visited pages in the history or store web page icons. -|<>|Enable extra tools for Web developers. -|<>|Whether the background color and images are also drawn when the page is printed. -|<>|Whether load requests should be monitored for cross-site scripting attempts. -|<>|Enable workarounds for broken sites. -|<>|Default encoding to use for websites. +|<>|Whether to find text on a page case-insensitively. +|<>|Whether to wrap finding text to the top when arriving at the end. +|<>|The default page(s) to open at the start, separated by commas. +|<>|Whether to start a search when something else than a URL is entered. +|<>|Whether to save the config automatically on quit. +|<>|The editor (and arguments) to use for the `open-editor` command. +|<>|Encoding to use for editor. +|<>|Do not record visited pages in the history or store web page icons. +|<>|Enable extra tools for Web developers. +|<>|Whether the background color and images are also drawn when the page is printed. +|<>|Whether load requests should be monitored for cross-site scripting attempts. +|<>|Enable workarounds for broken sites. +|<>|Default encoding to use for websites. |============== .Quick reference for section ``ui'' [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description -|<>|The available zoom levels, separated by commas. -|<>|The default zoom level. -|<>|Time (in ms) to show messages in the statusbar for. -|<>|Whether to confirm quitting the application. -|<>|Whether to display javascript statusbar messages. -|<>|Whether the zoom factor on a frame applies only to the text or to all content. -|<>|Whether to expand each subframe to its contents. -|<>|User stylesheet to use. -|<>|Set the CSS media type. +|<>|The available zoom levels, separated by commas. +|<>|The default zoom level. +|<>|Time (in ms) to show messages in the statusbar for. +|<>|Whether to confirm quitting the application. +|<>|Whether to display javascript statusbar messages. +|<>|Whether the zoom factor on a frame applies only to the text or to all content. +|<>|Whether to expand each subframe to its contents. +|<>|User stylesheet to use. +|<>|Set the CSS media type. |============== .Quick reference for section ``network'' [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description -|<>|Value to send in the `DNT` header. -|<>|Value to send in the `accept-language` header. -|<>|User agent to send. Empty to send the default. -|<>|The proxy to use. -|<>|Whether to validate SSL handshakes. -|<>|Whether to try to pre-fetch DNS entries to speed up browsing. +|<>|Value to send in the `DNT` header. +|<>|Value to send in the `accept-language` header. +|<>|User agent to send. Empty to send the default. +|<>|The proxy to use. +|<>|Whether to validate SSL handshakes. +|<>|Whether to try to pre-fetch DNS entries to speed up browsing. |============== .Quick reference for section ``completion'' [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description -|<>|Whether to show the autocompletion window. -|<>|The height of the completion, in px or as percentage of the window. -|<>|How many commands to save in the history. -|<>|Whether to move on to the next part when there's only one possible completion left. -|<>|Whether to shrink the completion to be smaller than the configured size if there are no scrollbars. +|<>|Whether to show the autocompletion window. +|<>|The height of the completion, in px or as percentage of the window. +|<>|How many commands to save in the history. +|<>|Whether to move on to the next part when there's only one possible completion left. +|<>|Whether to shrink the completion to be smaller than the configured size if there are no scrollbars. |============== .Quick reference for section ``input'' [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description -|<>|Timeout for ambiguous keybindings. -|<>|Whether to switch to insert mode when clicking flash and other plugins. -|<>|Whether to leave insert mode if a non-editable element is clicked. -|<>|Whether to automatically enter insert mode if an editable element is focused after page load. -|<>|Whether to forward unbound keys to the webview in normal mode. -|<>|Enables or disables the Spatial Navigation feature -|<>|Whether hyperlinks should be included in the keyboard focus chain. +|<>|Timeout for ambiguous keybindings. +|<>|Whether to switch to insert mode when clicking flash and other plugins. +|<>|Whether to leave insert mode if a non-editable element is clicked. +|<>|Whether to automatically enter insert mode if an editable element is focused after page load. +|<>|Whether to forward unbound keys to the webview in normal mode. +|<>|Enables or disables the Spatial Navigation feature +|<>|Whether hyperlinks should be included in the keyboard focus chain. |============== .Quick reference for section ``tabs'' [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description -|<>|Whether to open new tabs (middleclick/ctrl+click) in background. -|<>|Which tab to select when the focused tab is removed. -|<>|How new tabs are positioned. -|<>|How new tabs opened explicitely are positioned. -|<>|Behaviour when the last tab is closed. -|<>|Whether to wrap when changing tabs. -|<>|Whether tabs should be movable. -|<>|On which mouse button to close tabs. -|<>|The position of the tab bar. -|<>|Whether to show favicons in the tab bar. -|<>|The width of the tab bar if it's vertical, in px or as percentage of the window. -|<>|Width of the progress indicator (0 to disable). -|<>|Spacing between tab edge and indicator. +|<>|Whether to open new tabs (middleclick/ctrl+click) in background. +|<>|Which tab to select when the focused tab is removed. +|<>|How new tabs are positioned. +|<>|How new tabs opened explicitely are positioned. +|<>|Behaviour when the last tab is closed. +|<>|Whether to wrap when changing tabs. +|<>|Whether tabs should be movable. +|<>|On which mouse button to close tabs. +|<>|The position of the tab bar. +|<>|Whether to show favicons in the tab bar. +|<>|The width of the tab bar if it's vertical, in px or as percentage of the window. +|<>|Width of the progress indicator (0 to disable). +|<>|Spacing between tab edge and indicator. |============== .Quick reference for section ``storage'' [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description -|<>|The directory to save downloads to. An empty value selects a sensible os-specific default. -|<>|The maximum number of pages to hold in the memory page cache. -|<>|The capacities for the memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity. -|<>|Default quota for new offline storage databases. -|<>|Quota for the offline web application cache. -|<>|Whether support for the HTML 5 offline storage feature is enabled. -|<>|Whether support for the HTML 5 web application cache feature is enabled. -|<>|Whether support for the HTML 5 local storage feature is enabled. -|<>|Size of the HTTP network cache. +|<>|The directory to save downloads to. An empty value selects a sensible os-specific default. +|<>|The maximum number of pages to hold in the memory page cache. +|<>|The capacities for the memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity. +|<>|Default quota for new offline storage databases. +|<>|Quota for the offline web application cache. +|<>|Whether support for the HTML 5 offline storage feature is enabled. +|<>|Whether support for the HTML 5 web application cache feature is enabled. +|<>|Whether support for the HTML 5 local storage feature is enabled. +|<>|Size of the HTTP network cache. |============== .Quick reference for section ``permissions'' [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description -|<>|Whether images are automatically loaded in web pages. -|<>|Enables or disables the running of JavaScript programs. -|<>|Enables or disables plugins in Web pages. -|<>|Whether JavaScript programs can open new windows. -|<>|Whether JavaScript programs can close windows. -|<>|Whether JavaScript programs can read or write to the clipboard. -|<>|Whether locally loaded documents are allowed to access remote urls. -|<>|Whether locally loaded documents are allowed to access other local urls. -|<>|Whether to accept cookies. -|<>|Whether to store cookies. +|<>|Whether images are automatically loaded in web pages. +|<>|Enables or disables the running of JavaScript programs. +|<>|Enables or disables plugins in Web pages. +|<>|Whether JavaScript programs can open new windows. +|<>|Whether JavaScript programs can close windows. +|<>|Whether JavaScript programs can read or write to the clipboard. +|<>|Whether locally loaded documents are allowed to access remote urls. +|<>|Whether locally loaded documents are allowed to access other local urls. +|<>|Whether to accept cookies. +|<>|Whether to store cookies. |============== .Quick reference for section ``hints'' [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description -|<>|CSS border value for hints. -|<>|Opacity for hints. -|<>|Mode to use for hints. -|<>|Chars used for hint strings. -|<>|Whether to auto-follow a hint if there's only one left. -|<>|A comma-separated list of regexes to use for 'next' links. -|<>|A comma-separated list of regexes to use for 'prev' links. +|<>|CSS border value for hints. +|<>|Opacity for hints. +|<>|Mode to use for hints. +|<>|Chars used for hint strings. +|<>|Whether to auto-follow a hint if there's only one left. +|<>|A comma-separated list of regexes to use for 'next' links. +|<>|A comma-separated list of regexes to use for 'prev' links. |============== .Quick reference for section ``colors'' [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description -|<>|Text color of the completion widget. -|<>|Background color of the completion widget. -|<>|Background color of completion widget items. -|<>|Foreground color of completion widget category headers. -|<>|Background color of the completion widget category headers. -|<>|Top border color of the completion widget category headers. -|<>|Bottom border color of the completion widget category headers. -|<>|Foreground color of the selected completion item. -|<>|Background color of the selected completion item. -|<>|Top border color of the completion widget category headers. -|<>|Bottom border color of the selected completion item. -|<>|Foreground color of the matched text in the completion. -|<>|Foreground color of the statusbar. -|<>|Foreground color of the statusbar. -|<>|Background color of the statusbar if there was an error. -|<>|Background color of the statusbar if there is a prompt. -|<>|Background color of the statusbar in insert mode. -|<>|Background color of the progress bar. -|<>|Default foreground color of the URL in the statusbar. -|<>|Foreground color of the URL in the statusbar on successful load. -|<>|Foreground color of the URL in the statusbar on error. -|<>|Foreground color of the URL in the statusbar when there's a warning. -|<>|Foreground color of the URL in the statusbar for hovered links. -|<>|Foreground color of tabs. -|<>|Background color of unselected odd tabs. -|<>|Background color of unselected even tabs. -|<>|Background color of selected tabs. -|<>|Background color of the tabbar. -|<>|Color gradient start for the tab indicator. -|<>|Color gradient end for the tab indicator. -|<>|Color for the tab indicator on errors.. -|<>|Color gradient interpolation system for the tab indicator. -|<>|Color for the tab seperator. -|<>|Font color for hints. -|<>|Font color for the matched part of hints. -|<>|Background color for hints. -|<>|Foreground color for downloads. -|<>|Background color for the download bar. -|<>|Color gradient start for downloads. -|<>|Color gradient end for downloads. -|<>|Color gradient interpolation system for downloads. +|<>|Text color of the completion widget. +|<>|Background color of the completion widget. +|<>|Background color of completion widget items. +|<>|Foreground color of completion widget category headers. +|<>|Background color of the completion widget category headers. +|<>|Top border color of the completion widget category headers. +|<>|Bottom border color of the completion widget category headers. +|<>|Foreground color of the selected completion item. +|<>|Background color of the selected completion item. +|<>|Top border color of the completion widget category headers. +|<>|Bottom border color of the selected completion item. +|<>|Foreground color of the matched text in the completion. +|<>|Foreground color of the statusbar. +|<>|Foreground color of the statusbar. +|<>|Background color of the statusbar if there was an error. +|<>|Background color of the statusbar if there is a prompt. +|<>|Background color of the statusbar in insert mode. +|<>|Background color of the progress bar. +|<>|Default foreground color of the URL in the statusbar. +|<>|Foreground color of the URL in the statusbar on successful load. +|<>|Foreground color of the URL in the statusbar on error. +|<>|Foreground color of the URL in the statusbar when there's a warning. +|<>|Foreground color of the URL in the statusbar for hovered links. +|<>|Foreground color of tabs. +|<>|Background color of unselected odd tabs. +|<>|Background color of unselected even tabs. +|<>|Background color of selected tabs. +|<>|Background color of the tabbar. +|<>|Color gradient start for the tab indicator. +|<>|Color gradient end for the tab indicator. +|<>|Color for the tab indicator on errors.. +|<>|Color gradient interpolation system for the tab indicator. +|<>|Color for the tab seperator. +|<>|Font color for hints. +|<>|Font color for the matched part of hints. +|<>|Background color for hints. +|<>|Foreground color for downloads. +|<>|Background color for the download bar. +|<>|Color gradient start for downloads. +|<>|Color gradient end for downloads. +|<>|Color gradient interpolation system for downloads. |============== .Quick reference for section ``fonts'' [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description -|<>|Default monospace fonts. -|<>|Font used in the completion widget. -|<>|Font used in the tabbar. -|<>|Font used in the statusbar. -|<>|Font used for the downloadbar. -|<>|Font used for the hints. -|<>|Font used for the debugging console. -|<>|Font family for standard fonts. -|<>|Font family for fixed fonts. -|<>|Font family for serif fonts. -|<>|Font family for sans-serif fonts. -|<>|Font family for cursive fonts. -|<>|Font family for fantasy fonts. -|<>|The hard minimum font size. -|<>|The minimum logical font size that is applied when zooming out. -|<>|The default font size for regular text. -|<>|The default font size for fixed-pitch text. +|<>|Default monospace fonts. +|<>|Font used in the completion widget. +|<>|Font used in the tabbar. +|<>|Font used in the statusbar. +|<>|Font used for the downloadbar. +|<>|Font used for the hints. +|<>|Font used for the debugging console. +|<>|Font family for standard fonts. +|<>|Font family for fixed fonts. +|<>|Font family for serif fonts. +|<>|Font family for sans-serif fonts. +|<>|Font family for cursive fonts. +|<>|Font family for fantasy fonts. +|<>|The hard minimum font size. +|<>|The minimum logical font size that is applied when zooming out. +|<>|The default font size for regular text. +|<>|The default font size for fixed-pitch text. |============== == general diff --git a/scripts/src2asciidoc.py b/scripts/src2asciidoc.py index 8d32b136c..13e0741b1 100755 --- a/scripts/src2asciidoc.py +++ b/scripts/src2asciidoc.py @@ -139,7 +139,7 @@ def _get_setting_quickref(): out.append('|Setting|Description') for optname, _option in sect.items(): desc = sect.descriptions[optname].splitlines()[0] - out.append('|<>|{}'.format( + out.append('|<<{}-{},{}>>|{}'.format( sectname, optname, optname, desc)) out.append('|==============') return '\n'.join(out) From def417b8a550ba3975510e799aa592c36aa8addf Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 21 Sep 2014 22:15:56 +0200 Subject: [PATCH 83/89] Check if docs are up to date if running from git repo. --- qutebrowser/network/qutescheme.py | 7 ++++++- qutebrowser/utils/utils.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py index cb9e9ae6d..e3297aefc 100644 --- a/qutebrowser/network/qutescheme.py +++ b/qutebrowser/network/qutescheme.py @@ -31,7 +31,7 @@ from PyQt5.QtNetwork import QNetworkReply import qutebrowser from qutebrowser.network import schemehandler -from qutebrowser.utils import version, utils, jinja, log +from qutebrowser.utils import version, utils, jinja, log, message pyeval_output = ":pyeval was never called" @@ -137,6 +137,11 @@ def qute_help(request): urlpath = request.url().path() if not urlpath or urlpath == '/': urlpath = 'index.html' + else: + urlpath = urlpath.lstrip('/') + if not utils.docs_up_to_date(urlpath): + message.error("Your documentation is outdated! Please re-run scripts/" + "asciidoc2html.py.") path = 'html/doc/{}'.format(urlpath) return utils.read_file(path).encode('UTF-8', errors='xmlcharrefreplace') diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index fd7e35863..feef84e61 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -507,6 +507,36 @@ def is_enum(obj): return False +def is_git_repo(): + """Check if we're running from a git repository.""" + gitfolder = os.path.join(qutebrowser.basedir, os.path.pardir, '.git') + return os.path.isdir(gitfolder) + + +def docs_up_to_date(path): + """Check if the generated html documentation is up to date. + + Args: + path: The path of the document to check. + + Return: + True if they are up to date or we couldn't check. + False if they are outdated. + """ + if hasattr(sys, 'frozen') or not is_git_repo(): + return True + html_path = os.path.join(qutebrowser.basedir, 'html', 'doc', path) + filename = os.path.splitext(path)[0] + asciidoc_path = os.path.join(qutebrowser.basedir, os.path.pardir, + 'doc', 'help', filename + '.asciidoc') + try: + html_time = os.path.getmtime(html_path) + asciidoc_time = os.path.getmtime(asciidoc_path) + except FileNotFoundError: + return True + return asciidoc_time <= html_time + + class DocstringParser: """Generate documentation based on a docstring of a command handler. From 42e8e800aa2c31d3f067d12e75e7eb39cee90e12 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 22 Sep 2014 07:07:09 +0200 Subject: [PATCH 84/89] Start patching setuptools commands --- scripts/setupcommon.py | 18 ++++++++++++++++++ setup.py | 14 ++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/scripts/setupcommon.py b/scripts/setupcommon.py index 89ab8e81c..7ecd331d8 100644 --- a/scripts/setupcommon.py +++ b/scripts/setupcommon.py @@ -92,6 +92,24 @@ def write_git_file(): f.write(gitstr) +def patch_docgen(cls): + """Class decorator to patch a setuptool command to generate docs. + + Based on: + http://www.niteoweb.com/blog/setuptools-run-custom-code-during-install + """ + + orig_run = cls.run + + def run(*args, **kwargs): + from scripts import asciidoc2html + asciidoc2html.main(colors=False) + return orig_run(*args, **kwargs) + + cls.run = run + return cls + + setupdata = { 'name': 'qutebrowser', 'version': '.'.join(map(str, _get_constant('version_info'))), diff --git a/setup.py b/setup.py index 2c553b71f..a20daae03 100755 --- a/setup.py +++ b/setup.py @@ -29,6 +29,11 @@ from scripts import setupcommon as common from scripts import ez_setup ez_setup.use_setuptools() import setuptools +from setuptools.command.sdist import sdist as cmd_sdist +from setuptools.command.bdist_rpm import bdist_rpm as cmd_bdist_rpm +from setuptools.command.bdist_wininst import bdist_wininst as cmd_bdist_wininst +from setuptools.command.bdist_egg import bdist_egg as cmd_bdist_egg +from setuptools.command.develop import develop as cmd_develop try: @@ -37,6 +42,14 @@ except NameError: BASEDIR = None +command_classes = {} +command_classes['sdist'] = common.patch_docgen(cmd_sdist) +command_classes['bdist_rpm'] = common.patch_docgen(cmd_bdist_rpm) +command_classes['bdist_wininst'] = common.patch_docgen(cmd_bdist_wininst) +command_classes['bdist_egg'] = common.patch_docgen(cmd_bdist_egg) +command_classes['develop'] = common.patch_docgen(cmd_develop) + + try: common.write_git_file() setuptools.setup( @@ -50,6 +63,7 @@ try: extras_require={'nice-debugging': ['colorlog', 'colorama'], 'checks': ['flake8', 'pylint', 'check-manifest', 'pyroma']}, + cmdclass=command_classes, **common.setupdata ) finally: From e552b62feb7c03ccd33aeaddd1595aa6f24b3cb7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 22 Sep 2014 18:31:19 +0200 Subject: [PATCH 85/89] Revert "Start patching setuptools commands" This reverts commit 42e8e800aa2c31d3f067d12e75e7eb39cee90e12. --- scripts/setupcommon.py | 18 ------------------ setup.py | 14 -------------- 2 files changed, 32 deletions(-) diff --git a/scripts/setupcommon.py b/scripts/setupcommon.py index 7ecd331d8..89ab8e81c 100644 --- a/scripts/setupcommon.py +++ b/scripts/setupcommon.py @@ -92,24 +92,6 @@ def write_git_file(): f.write(gitstr) -def patch_docgen(cls): - """Class decorator to patch a setuptool command to generate docs. - - Based on: - http://www.niteoweb.com/blog/setuptools-run-custom-code-during-install - """ - - orig_run = cls.run - - def run(*args, **kwargs): - from scripts import asciidoc2html - asciidoc2html.main(colors=False) - return orig_run(*args, **kwargs) - - cls.run = run - return cls - - setupdata = { 'name': 'qutebrowser', 'version': '.'.join(map(str, _get_constant('version_info'))), diff --git a/setup.py b/setup.py index a20daae03..2c553b71f 100755 --- a/setup.py +++ b/setup.py @@ -29,11 +29,6 @@ from scripts import setupcommon as common from scripts import ez_setup ez_setup.use_setuptools() import setuptools -from setuptools.command.sdist import sdist as cmd_sdist -from setuptools.command.bdist_rpm import bdist_rpm as cmd_bdist_rpm -from setuptools.command.bdist_wininst import bdist_wininst as cmd_bdist_wininst -from setuptools.command.bdist_egg import bdist_egg as cmd_bdist_egg -from setuptools.command.develop import develop as cmd_develop try: @@ -42,14 +37,6 @@ except NameError: BASEDIR = None -command_classes = {} -command_classes['sdist'] = common.patch_docgen(cmd_sdist) -command_classes['bdist_rpm'] = common.patch_docgen(cmd_bdist_rpm) -command_classes['bdist_wininst'] = common.patch_docgen(cmd_bdist_wininst) -command_classes['bdist_egg'] = common.patch_docgen(cmd_bdist_egg) -command_classes['develop'] = common.patch_docgen(cmd_develop) - - try: common.write_git_file() setuptools.setup( @@ -63,7 +50,6 @@ try: extras_require={'nice-debugging': ['colorlog', 'colorama'], 'checks': ['flake8', 'pylint', 'check-manifest', 'pyroma']}, - cmdclass=command_classes, **common.setupdata ) finally: From ef1c40d00be95171ae9164fecaad5ec9f9189d37 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 22 Sep 2014 18:32:33 +0200 Subject: [PATCH 86/89] Add note about docs to README --- README.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.asciidoc b/README.asciidoc index 90e7e6e34..3f3e96eeb 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -46,6 +46,10 @@ After installing the <>, you have these options: * Run `python3 setup.py install` to install qutebrowser, then call `qutebrowser`. +NOTE: If you're running qutebrowser from the git repository rather than a +released version, you should run `scripts/asciidoc2html.py` to generate the +documentation. + Contributions / Bugs -------------------- From 4030976f3bf793ef204d69f8e91fbb7c1381e8b0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 22 Sep 2014 18:33:19 +0200 Subject: [PATCH 87/89] Add HTML documentation to .gitignore. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6f8b588a7..3cb08b141 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,6 @@ __pycache__ /setuptools-*.egg /setuptools-*.zip /qutebrowser/git-commit-id -# We can probably remove these later /doc/*.html /README.html +/qutebrowser/html/doc/ From c393a375d0b99d76c7fb10d3efce4618ef1d1881 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 22 Sep 2014 18:34:40 +0200 Subject: [PATCH 88/89] Remove README/manpage from asciidoc2html.py --- scripts/asciidoc2html.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index e43173c02..179989d1f 100644 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -57,8 +57,6 @@ def call_asciidoc(src, dst): def main(colors=False): utils.use_color = colors asciidoc_files = [ - ('doc/qutebrowser.1.asciidoc', None), - ('README.asciidoc', None), ('doc/FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'), ] try: From cb53432969f14c110183a94c40d93056d6e0930d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 22 Sep 2014 18:45:21 +0200 Subject: [PATCH 89/89] Add doc building to PKGBUILD --- pkg/PKGBUILD.qutebrowser-git | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/PKGBUILD.qutebrowser-git b/pkg/PKGBUILD.qutebrowser-git index ff7715c6a..54056f5ba 100644 --- a/pkg/PKGBUILD.qutebrowser-git +++ b/pkg/PKGBUILD.qutebrowser-git @@ -23,6 +23,7 @@ pkgver() { package() { cd "$srcdir/qutebrowser" + python scripts/asciidoc2html.py python setup.py install --root="$pkgdir/" --optimize=1 a2x -f manpage doc/qutebrowser.1.asciidoc install -Dm644 doc/qutebrowser.1 "$pkgdir/usr/share/man/man1/qutebrowser.1"