Start initial newcmd stuff.

This commit is contained in:
Florian Bruhin 2014-09-02 21:54:07 +02:00
parent 84a034d7e9
commit a811f8cb07
12 changed files with 380 additions and 360 deletions

View File

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

View File

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

View File

@ -0,0 +1,40 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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)

View File

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

View File

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

View File

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

View File

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

View File

@ -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([
('<Ctrl-Q>', 'quit'),
('<Ctrl-Shift-T>', 'undo'),
('<Ctrl-W>', 'tab-close'),
('<Ctrl-T>', 'open-tab about:blank'),
('<Ctrl-T>', 'open -t about:blank'),
('<Ctrl-F>', 'scroll-page 0 1'),
('<Ctrl-B>', 'scroll-page 0 -1'),
('<Ctrl-D>', 'scroll-page 0 0.5'),

View File

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

View File

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

View File

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

View File

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