qutebrowser/qutebrowser/browser/commands.py

2200 lines
83 KiB
Python
Raw Normal View History

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2014-06-19 09:04:37 +02:00
2017-05-09 21:37:03 +02:00
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2014-04-17 09:44:26 +02:00
#
# 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/>.
2014-05-27 15:56:44 +02:00
"""Command dispatcher for TabbedBrowser."""
2014-04-17 09:44:26 +02:00
2014-04-29 18:00:22 +02:00
import os
2015-11-24 18:04:10 +01:00
import os.path
import shlex
2014-09-28 22:13:14 +02:00
import functools
2017-09-18 09:41:12 +02:00
import typing
2014-04-29 18:00:22 +02:00
2017-07-11 23:41:08 +02:00
from PyQt5.QtWidgets import QApplication, QTabBar, QDialog
from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
2014-09-15 17:59:54 +02:00
import pygments
import pygments.lexers
import pygments.formatters
2014-04-17 09:44:26 +02:00
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.config import config, configdata
from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
2016-11-01 17:50:29 +01:00
webelem, downloads)
2015-05-13 07:29:17 +02:00
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
2017-09-18 09:41:12 +02:00
objreg, utils, debug)
2015-04-28 16:50:42 +02:00
from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess
from qutebrowser.completion.models import urlmodel, miscmodels
2017-09-18 09:41:12 +02:00
from qutebrowser.mainwindow import mainwindow
2014-04-17 09:44:26 +02:00
class CommandDispatcher:
2014-04-17 09:44:26 +02:00
"""Command dispatcher for TabbedBrowser.
Contains all commands which are related to the current tab.
We can't simply add these commands to BrowserTab directly and use
2014-05-17 22:38:07 +02:00
currentWidget() for TabbedBrowser.cmd because at the time
2014-04-17 09:44:26 +02:00
cmdutils.register() decorators are run, currentWidget() will return None.
Attributes:
_editor: The ExternalEditor object.
2014-09-28 22:13:14 +02:00
_win_id: The window ID the CommandDispatcher is associated with.
_tabbed_browser: The TabbedBrowser used.
2014-04-17 09:44:26 +02:00
"""
def __init__(self, win_id, tabbed_browser):
2014-09-28 22:13:14 +02:00
self._win_id = win_id
self._tabbed_browser = tabbed_browser
2014-04-17 09:44:26 +02:00
2014-09-23 23:31:17 +02:00
def __repr__(self):
2014-09-26 15:48:24 +02:00
return utils.get_repr(self)
2014-09-23 23:31:17 +02:00
2017-04-24 21:08:00 +02:00
def _new_tabbed_browser(self, private):
"""Get a tabbed-browser from a new window."""
2017-04-24 21:08:00 +02:00
new_window = mainwindow.MainWindow(private=private)
new_window.show()
return new_window.tabbed_browser
2014-09-28 22:13:14 +02:00
def _count(self):
"""Convenience method to get the widget count."""
return self._tabbed_browser.count()
def _set_current_index(self, idx):
"""Convenience method to set the current widget index."""
cmdutils.check_overflow(idx, 'int')
2016-08-03 09:07:12 +02:00
self._tabbed_browser.setCurrentIndex(idx)
def _current_index(self):
"""Convenience method to get the current widget index."""
return self._tabbed_browser.currentIndex()
def _current_url(self):
"""Convenience method to get the current url."""
try:
return self._tabbed_browser.current_url()
except qtutils.QtValueError as e:
msg = "Current URL is invalid"
if e.reason:
msg += " ({})".format(e.reason)
msg += "!"
raise cmdexc.CommandError(msg)
2015-05-22 00:17:22 +02:00
def _current_title(self):
"""Convenience method to get the current title."""
2015-07-12 02:28:31 +02:00
return self._current_widget().title()
2015-05-22 00:17:22 +02:00
def _current_widget(self):
"""Get the currently active widget from a command."""
widget = self._tabbed_browser.currentWidget()
if widget is None:
raise cmdexc.CommandError("No WebView available yet!")
return widget
def _open(self, url, tab=False, background=False, window=False,
related=False, private=None):
"""Helper function to open a page.
Args:
url: The URL to open as QUrl.
tab: Whether to open in a new tab.
background: Whether to open in the background.
window: Whether to open in a new window
2017-04-24 21:08:00 +02:00
private: If opening a new window, open it in private browsing mode.
If not given, inherit the current window's mode.
"""
urlutils.raise_cmdexc_if_invalid(url)
tabbed_browser = self._tabbed_browser
2017-05-10 22:44:05 +02:00
cmdutils.check_exclusive((tab, background, window, private), 'tbwp')
if window and private is None:
2017-04-24 21:08:00 +02:00
private = self._tabbed_browser.private
if window or private:
2017-04-24 21:08:00 +02:00
tabbed_browser = self._new_tabbed_browser(private)
tabbed_browser.tabopen(url)
elif tab:
tabbed_browser.tabopen(url, background=False, related=related)
elif background:
tabbed_browser.tabopen(url, background=True, related=related)
else:
widget = self._current_widget()
widget.openurl(url)
2014-09-24 19:53:31 +02:00
def _cntwidget(self, count=None):
"""Return a widget based on a count/idx.
Args:
count: The tab index, or None.
Return:
The current widget if count is None.
The widget with the given tab ID if count is given.
None if no widget was found.
"""
if count is None:
return self._tabbed_browser.currentWidget()
elif 1 <= count <= self._count():
2014-09-24 19:53:31 +02:00
cmdutils.check_overflow(count + 1, 'int')
return self._tabbed_browser.widget(count - 1)
2014-09-24 19:53:31 +02:00
else:
return None
2017-06-20 12:45:36 +02:00
def _tab_focus_last(self, *, show_error=True):
2014-08-03 00:39:39 +02:00
"""Select the tab which was last focused."""
try:
tab = objreg.get('last-focused-tab', scope='window',
window=self._win_id)
except KeyError:
if not show_error:
return
2014-08-26 19:10:14 +02:00
raise cmdexc.CommandError("No last focused tab!")
idx = self._tabbed_browser.indexOf(tab)
2014-08-03 00:39:39 +02:00
if idx == -1:
2014-08-26 19:10:14 +02:00
raise cmdexc.CommandError("Last focused tab vanished!")
self._set_current_index(idx)
2014-08-03 00:39:39 +02:00
def _get_selection_override(self, prev, next_, opposite):
"""Helper function for tab_close to get the tab to select.
Args:
prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab.
2015-03-31 20:49:29 +02:00
opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs.select_on_remove'.
Return:
QTabBar.SelectLeftTab, QTabBar.SelectRightTab, or None if no change
should be made.
"""
cmdutils.check_exclusive((prev, next_, opposite), 'pno')
if prev:
return QTabBar.SelectLeftTab
elif next_:
return QTabBar.SelectRightTab
elif opposite:
conf_selection = config.val.tabs.select_on_remove
if conf_selection == QTabBar.SelectLeftTab:
return QTabBar.SelectRightTab
elif conf_selection == QTabBar.SelectRightTab:
return QTabBar.SelectLeftTab
elif conf_selection == QTabBar.SelectPreviousTab:
raise cmdexc.CommandError(
"-o is not supported with 'tabs.select_on_remove' set to "
"'last-used'!")
2015-11-25 18:47:36 +01:00
else: # pragma: no cover
raise ValueError("Invalid select_on_remove value "
2015-11-25 18:47:36 +01:00
"{!r}!".format(conf_selection))
return None
def _tab_close(self, tab, prev=False, next_=False, opposite=False):
"""Helper function for tab_close be able to handle message.async.
2014-05-17 22:38:07 +02:00
Args:
2017-05-14 09:21:51 +02:00
tab: Tab object to select be closed.
prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab.
2015-03-31 20:49:29 +02:00
opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs.select_on_remove'.
2014-05-17 22:38:07 +02:00
count: The tab index to close, or None
"""
tabbar = self._tabbed_browser.tabBar()
selection_override = self._get_selection_override(prev, next_,
opposite)
if selection_override is None:
self._tabbed_browser.close_tab(tab)
else:
old_selection_behavior = tabbar.selectionBehaviorOnRemove()
tabbar.setSelectionBehaviorOnRemove(selection_override)
self._tabbed_browser.close_tab(tab)
tabbar.setSelectionBehaviorOnRemove(old_selection_behavior)
2014-05-17 22:38:07 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def tab_close(self, prev=False, next_=False, opposite=False,
2016-11-22 06:57:00 +01:00
force=False, count=None):
2014-05-17 22:38:07 +02:00
"""Close the current/[count]th tab.
2014-05-17 22:38:07 +02:00
Args:
prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab.
2015-03-31 20:49:29 +02:00
opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs.select_on_remove'.
force: Avoid confirmation for pinned tabs.
2014-05-17 22:38:07 +02:00
count: The tab index to close, or None
"""
2014-09-24 19:53:31 +02:00
tab = self._cntwidget(count)
2014-05-17 22:38:07 +02:00
if tab is None:
return
close = functools.partial(self._tab_close, tab, prev,
next_, opposite)
self._tabbed_browser.tab_close_prompt_if_pinned(tab, force, close)
2016-11-07 21:25:05 +01:00
@cmdutils.register(instance='command-dispatcher', scope='window',
name='tab-pin')
@cmdutils.argument('count', count=True)
def tab_pin(self, count=None):
2017-05-17 06:03:33 +02:00
"""Pin/Unpin the current/[count]th tab.
2017-07-23 01:20:27 +02:00
Pinning a tab shrinks it to the size of its title text.
2017-05-22 07:11:13 +02:00
Attempting to close a pinned tab will cause a confirmation,
unless --force is passed.
Args:
count: The tab index to pin or unpin, or None
"""
tab = self._cntwidget(count)
if tab is None:
return
to_pin = not tab.data.pinned
self._tabbed_browser.set_tab_pinned(tab, to_pin)
@cmdutils.register(instance='command-dispatcher', name='open',
2016-05-10 23:04:52 +02:00
maxsplit=0, scope='window')
@cmdutils.argument('url', completion=urlmodel.url)
@cmdutils.argument('count', count=True)
def openurl(self, url=None, related=False,
2017-04-24 21:08:00 +02:00
bg=False, tab=False, window=False, count=None, secure=False,
private=False):
"""Open a URL in the current/[count]th tab.
2014-04-17 09:44:26 +02:00
If the URL contains newlines, each line gets opened in its own tab.
2014-04-17 09:44:26 +02:00
Args:
2014-09-13 00:22:27 +02:00
url: The URL to open.
bg: Open in a new background tab.
tab: Open in a new tab.
window: Open in a new window.
related: If opening a new tab, position the tab as related to the
current one (like clicking on a link).
2014-04-17 09:44:26 +02:00
count: The tab index to open the URL in, or None.
2017-03-25 08:44:36 +01:00
secure: Force HTTPS.
2017-04-24 21:08:00 +02:00
private: Open a new window in private browsing mode.
2014-04-17 09:44:26 +02:00
"""
if url is None:
2017-07-02 12:07:27 +02:00
urls = [config.val.url.default_page]
else:
urls = self._parse_url_input(url)
2016-11-26 12:17:23 +01:00
for i, cur_url in enumerate(urls):
2017-03-25 08:44:36 +01:00
if secure:
cur_url.setScheme('https')
if not window and i > 0:
tab = False
bg = True
if tab or bg or window or private:
self._open(cur_url, tab, bg, window, related=related,
2017-04-24 21:08:00 +02:00
private=private)
2014-09-02 21:54:07 +02:00
else:
curtab = self._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._tabbed_browser.tabopen(cur_url)
else:
# Explicit count with a tab that doesn't exist.
return
elif curtab.data.pinned:
message.info("Tab is pinned!")
else:
curtab.openurl(cur_url)
def _parse_url(self, url, *, force_search=False):
"""Parse a URL or quickmark or search query.
Args:
url: The URL to parse.
force_search: Whether to force a search even if the content can be
interpreted as a URL or a path.
Return:
2016-08-09 19:23:38 +02:00
A URL that can be opened.
"""
try:
return objreg.get('quickmark-manager').get(url)
except urlmarks.Error:
try:
return urlutils.fuzzy_url(url, force_search=force_search)
except urlutils.InvalidUrlError as e:
# We don't use cmdexc.CommandError here as this can be
# called async from edit_url
2016-09-14 20:52:32 +02:00
message.error(str(e))
return None
def _parse_url_input(self, url):
"""Parse a URL or newline-separated list of URLs.
Args:
url: The URL or list to parse.
Return:
2016-08-09 19:23:38 +02:00
A list of URLs that can be opened.
"""
if isinstance(url, QUrl):
yield url
return
force_search = False
urllist = [u for u in url.split('\n') if u.strip()]
if (len(urllist) > 1 and not urlutils.is_url(urllist[0]) and
urlutils.get_path_if_valid(urllist[0], check_exists=True)
is None):
urllist = [url]
force_search = True
for cur_url in urllist:
parsed = self._parse_url(cur_url, force_search=force_search)
if parsed is not None:
yield parsed
2014-04-17 09:44:26 +02:00
2014-09-28 22:13:14 +02:00
@cmdutils.register(instance='command-dispatcher', name='reload',
scope='window')
@cmdutils.argument('count', count=True)
def reloadpage(self, force=False, count=None):
2014-04-17 09:44:26 +02:00
"""Reload the current/[count]th tab.
Args:
count: The tab index to reload, or None.
2014-12-19 15:27:22 +01:00
force: Bypass the page cache.
2014-04-17 09:44:26 +02:00
"""
2014-09-24 19:53:31 +02:00
tab = self._cntwidget(count)
2014-04-17 09:44:26 +02:00
if tab is not None:
2016-06-14 13:39:51 +02:00
tab.reload(force=force)
2014-04-17 09:44:26 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def stop(self, count=None):
2014-04-17 09:44:26 +02:00
"""Stop loading in the current/[count]th tab.
Args:
count: The tab index to stop, or None.
"""
2014-09-24 19:53:31 +02:00
tab = self._cntwidget(count)
2014-04-17 09:44:26 +02:00
if tab is not None:
tab.stop()
def _print_preview(self, tab):
"""Show a print preview."""
def print_callback(ok):
if not ok:
message.error("Printing failed!")
2017-02-06 13:42:31 +01:00
tab.printing.check_preview_support()
diag = QPrintPreviewDialog(tab)
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.setWindowFlags(diag.windowFlags() | Qt.WindowMaximizeButtonHint |
Qt.WindowMinimizeButtonHint)
diag.paintRequested.connect(functools.partial(
tab.printing.to_printer, callback=print_callback))
diag.exec_()
def _print_pdf(self, tab, filename):
"""Print to the given PDF file."""
2017-02-06 13:42:31 +01:00
tab.printing.check_pdf_support()
filename = os.path.expanduser(filename)
directory = os.path.dirname(filename)
if directory and not os.path.exists(directory):
os.mkdir(directory)
2017-02-06 13:09:39 +01:00
tab.printing.to_pdf(filename)
log.misc.debug("Print to file: {}".format(filename))
def _print(self, tab):
"""Print with a QPrintDialog."""
def print_callback(ok):
"""Called when printing finished."""
if not ok:
message.error("Printing failed!")
diag.deleteLater()
2017-07-11 23:41:08 +02:00
def do_print():
"""Called when the dialog was closed."""
tab.printing.to_printer(diag.printer(), print_callback)
diag = QPrintDialog(tab)
if utils.is_mac:
2017-07-11 23:41:08 +02:00
# For some reason we get a segfault when using open() on macOS
ret = diag.exec_()
if ret == QDialog.Accepted:
do_print()
else:
diag.open(do_print)
2014-09-28 22:13:14 +02:00
@cmdutils.register(instance='command-dispatcher', name='print',
scope='window')
@cmdutils.argument('count', count=True)
2016-07-12 02:07:54 +02:00
@cmdutils.argument('pdf', flag='f', metavar='file')
def printpage(self, preview=False, count=None, *, pdf=None):
2014-04-17 09:44:26 +02:00
"""Print the current/[count]th tab.
Args:
2014-09-13 00:22:27 +02:00
preview: Show preview instead of printing.
2014-04-17 09:44:26 +02:00
count: The tab index to print, or None.
pdf: The file path to write the PDF to.
2014-04-17 09:44:26 +02:00
"""
2014-09-24 19:53:31 +02:00
tab = self._cntwidget(count)
if tab is None:
return
try:
if pdf:
tab.printing.check_pdf_support()
2014-09-02 21:54:07 +02:00
else:
tab.printing.check_printer_support()
2017-02-06 09:51:11 +01:00
if preview:
tab.printing.check_preview_support()
except browsertab.WebTabError as e:
raise cmdexc.CommandError(e)
if preview:
self._print_preview(tab)
elif pdf:
self._print_pdf(tab, pdf)
else:
self._print(tab)
2014-04-17 09:44:26 +02:00
2014-09-28 22:13:14 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
2014-10-06 17:58:40 +02:00
def tab_clone(self, bg=False, window=False):
2014-09-27 23:13:11 +02:00
"""Duplicate the current tab.
Args:
bg: Open in a background tab.
2014-10-06 17:58:40 +02:00
window: Open in a new window.
2014-09-27 23:13:11 +02:00
Return:
The new QWebView.
"""
cmdutils.check_exclusive((bg, window), 'bw')
curtab = self._current_widget()
cur_title = self._tabbed_browser.page_title(self._current_index())
try:
history = curtab.history.serialize()
except browsertab.WebTabError as e:
raise cmdexc.CommandError(e)
# The new tab could be in a new tabbed_browser (e.g. because of
# tabs.tabs_are_windows being set)
if window:
2017-04-24 21:08:00 +02:00
new_tabbed_browser = self._new_tabbed_browser(
private=self._tabbed_browser.private)
else:
new_tabbed_browser = self._tabbed_browser
newtab = new_tabbed_browser.tabopen(background=bg)
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=newtab.win_id)
idx = new_tabbed_browser.indexOf(newtab)
new_tabbed_browser.set_page_title(idx, cur_title)
if config.val.tabs.favicons.show:
new_tabbed_browser.setTabIcon(idx, curtab.icon())
if config.val.tabs.tabs_are_windows:
new_tabbed_browser.window().setWindowIcon(curtab.icon())
2016-07-04 16:12:58 +02:00
newtab.data.keep_icon = True
newtab.history.deserialize(history)
newtab.zoom.set_factor(curtab.zoom.factor())
new_tabbed_browser.set_tab_pinned(newtab, curtab.data.pinned)
2016-06-14 15:42:32 +02:00
return newtab
2014-09-27 23:13:11 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', completion=miscmodels.buffer)
def tab_take(self, index):
"""Take a tab from another window.
Args:
index: The [win_id/]index of the tab to take. Or a substring
in which case the closest match will be taken.
"""
tabbed_browser, tab = self._resolve_buffer_index(index)
if tabbed_browser is self._tabbed_browser:
raise cmdexc.CommandError("Can't take a tab from the same window")
self._open(tab.url(), tab=True)
tabbed_browser.close_tab(tab, add_undo=False)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('win_id', completion=miscmodels.window)
2017-10-04 04:35:40 +02:00
def tab_give(self, win_id: int = None):
"""Give the current tab to a new or existing window if win_id given.
If no win_id is given, the tab will get detached into a new window.
Args:
win_id: The window ID of the window to give the current tab to.
"""
if win_id == self._win_id:
raise cmdexc.CommandError("Can't give a tab to the same window")
2017-10-17 09:14:59 +02:00
if win_id is None:
2017-10-04 04:35:40 +02:00
if self._count() < 2:
raise cmdexc.CommandError("Cannot detach from a window with "
"only one tab")
tabbed_browser = self._new_tabbed_browser(
2017-10-17 09:14:59 +02:00
private=self._tabbed_browser.private)
else:
if win_id not in objreg.window_registry:
raise cmdexc.CommandError(
"There's no window with id {}!".format(win_id))
2017-10-17 09:14:59 +02:00
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.tabopen(self._current_url())
self._tabbed_browser.close_tab(self._current_widget(), add_undo=False)
@cmdutils.register(instance='command-dispatcher', scope='window',
deprecated='Use :tab-give instead!')
2017-10-17 10:28:28 +02:00
def tab_detach(self):
"""Deprecated way to detach a tab."""
self.tab_give()
2014-10-06 17:58:40 +02:00
def _back_forward(self, tab, bg, window, count, forward):
"""Helper function for :back/:forward."""
2016-06-14 13:28:28 +02:00
history = self._current_widget().history
# Catch common cases before e.g. cloning tab
2016-06-14 13:28:28 +02:00
if not forward and not history.can_go_back():
raise cmdexc.CommandError("At beginning of history.")
2016-06-14 13:28:28 +02:00
elif forward and not history.can_go_forward():
raise cmdexc.CommandError("At end of history.")
2014-10-06 17:58:40 +02:00
if tab or bg or window:
widget = self.tab_clone(bg, window)
else:
widget = self._current_widget()
2017-07-10 15:58:11 +02:00
try:
if forward:
widget.history.forward(count)
else:
widget.history.back(count)
2017-07-10 15:58:11 +02:00
except browsertab.WebTabError as e:
raise cmdexc.CommandError(e)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def back(self, tab=False, bg=False, window=False, count=1):
2014-04-17 09:44:26 +02:00
"""Go back in the history of the current tab.
Args:
tab: Go back in a new tab.
bg: Go back in a background tab.
2014-10-06 17:58:40 +02:00
window: Go back in a new window.
2014-04-17 09:44:26 +02:00
count: How many pages to go back.
"""
2014-10-06 17:58:40 +02:00
self._back_forward(tab, bg, window, count, forward=False)
2014-04-17 09:44:26 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def forward(self, tab=False, bg=False, window=False, count=1):
2014-04-17 09:44:26 +02:00
"""Go forward in the history of the current tab.
Args:
tab: Go forward in a new tab.
2014-10-06 17:58:40 +02:00
bg: Go forward in a background tab.
window: Go forward in a new window.
2014-04-17 09:44:26 +02:00
count: How many pages to go forward.
"""
2014-10-06 17:58:40 +02:00
self._back_forward(tab, bg, window, count, forward=True)
2014-04-17 09:44:26 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment',
'decrement'])
@cmdutils.argument('count', count=True)
def navigate(self, where: str, tab=False, bg=False, window=False, count=1):
2014-09-22 21:13:42 +02:00
"""Open typical prev/next links or navigate using the URL path.
2014-08-03 00:33:39 +02:00
This tries to automatically click on typical _Previous Page_ or
_Next Page_ links using some heuristics.
2014-09-02 21:54:07 +02:00
2014-09-22 21:13:42 +02:00
Alternatively it can navigate by changing the current URL.
2014-09-02 21:54:07 +02:00
Args:
where: What to open.
2014-05-01 15:27:32 +02:00
- `prev`: Open a _previous_ link.
- `next`: Open a _next_ link.
2014-09-22 21:13:42 +02:00
- `up`: Go up a level in the current URL.
2014-09-22 21:51:09 +02:00
- `increment`: Increment the last number in the URL.
- `decrement`: Decrement the last number in the URL.
2014-08-03 00:33:39 +02:00
2014-09-13 00:22:27 +02:00
tab: Open in a new tab.
2014-10-06 17:58:40 +02:00
bg: Open in a background tab.
window: Open in a new window.
2016-08-16 11:36:55 +02:00
count: For `increment` and `decrement`, the number to change the
URL by. For `up`, the number of levels to go up in the URL.
2014-08-03 00:33:39 +02:00
"""
# save the pre-jump position in the special ' mark
self.set_mark("'")
cmdutils.check_exclusive((tab, bg, window), 'tbw')
2014-09-22 21:08:11 +02:00
widget = self._current_widget()
2016-04-25 19:05:20 +02:00
url = self._current_url().adjusted(QUrl.RemoveFragment)
2016-06-15 14:07:05 +02:00
handlers = {
'prev': functools.partial(navigate.prevnext, prev=True),
'next': functools.partial(navigate.prevnext, prev=False),
'up': navigate.path_up,
'decrement': functools.partial(navigate.incdec,
inc_or_dec='decrement'),
'increment': functools.partial(navigate.incdec,
inc_or_dec='increment'),
}
try:
if where in ['prev', 'next']:
handler = handlers[where]
handler(browsertab=widget, win_id=self._win_id, baseurl=url,
tab=tab, background=bg, window=window)
elif where in ['up', 'increment', 'decrement']:
new_url = handlers[where](url, count)
self._open(new_url, tab, bg, window, related=True)
else: # pragma: no cover
raise ValueError("Got called with invalid value {} for "
"`where'.".format(where))
except navigate.Error as e:
raise cmdexc.CommandError(e)
2014-05-01 15:27:32 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
2016-05-10 22:03:10 +02:00
def scroll_px(self, dx: int, dy: int, count=1):
"""Scroll the current tab by 'count * dx/dy' pixels.
2014-04-17 09:44:26 +02:00
Args:
dx: How much to scroll in x-direction.
2016-04-12 04:31:49 +02:00
dy: How much to scroll in y-direction.
2014-04-17 09:44:26 +02:00
count: multiplier
"""
2014-09-02 21:54:07 +02:00
dx *= count
dy *= count
cmdutils.check_overflow(dx, 'int')
cmdutils.check_overflow(dy, 'int')
self._current_widget().scroller.delta(dx, dy)
2014-04-17 09:44:26 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def scroll(self, direction: typing.Union[str, int], count=1):
"""Scroll the current tab in the given direction.
Note you can use `:run-with-count` to have a keybinding with a bigger
scroll increment.
Args:
direction: In which direction to scroll
(up/down/left/right/top/bottom).
count: multiplier
"""
2016-06-14 17:32:36 +02:00
tab = self._current_widget()
funcs = {
'up': tab.scroller.up,
'down': tab.scroller.down,
'left': tab.scroller.left,
'right': tab.scroller.right,
'top': tab.scroller.top,
'bottom': tab.scroller.bottom,
'page-up': tab.scroller.page_up,
'page-down': tab.scroller.page_down,
}
try:
2016-06-14 17:32:36 +02:00
func = funcs[direction]
except KeyError:
2016-06-14 17:32:36 +02:00
expected_values = ', '.join(sorted(funcs))
raise cmdexc.CommandError("Invalid value {!r} for direction - "
"expected one of: {}".format(
direction, expected_values))
if direction in ['top', 'bottom']:
2016-06-14 17:32:36 +02:00
func()
else:
func(count=count)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.argument('horizontal', flag='x')
def scroll_to_perc(self, perc: float = None, horizontal=False, count=None):
2014-09-02 21:54:07 +02:00
"""Scroll to a specific percentage of the page.
2014-08-03 00:33:39 +02:00
The percentage can be given either as argument or as count.
If no percentage is given, the page is scrolled to the end.
2014-04-17 09:44:26 +02:00
Args:
perc: Percentage to scroll.
2014-09-13 00:22:27 +02:00
horizontal: Scroll horizontally instead of vertically.
2014-04-17 09:44:26 +02:00
count: Percentage to scroll.
"""
# save the pre-jump position in the special ' mark
self.set_mark("'")
if perc is None and count is None:
perc = 100
2016-06-29 00:12:44 +02:00
elif count is not None:
perc = count
2016-06-14 17:32:36 +02:00
if horizontal:
x = perc
y = None
else:
2016-06-14 17:32:36 +02:00
x = None
y = perc
self._current_widget().scroller.to_perc(x, y)
2014-04-17 09:44:26 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.argument('top_navigate', metavar='ACTION',
choices=('prev', 'decrement'))
@cmdutils.argument('bottom_navigate', metavar='ACTION',
choices=('next', 'increment'))
2016-05-10 22:03:10 +02:00
def scroll_page(self, x: float, y: float, *,
2017-04-13 18:00:36 +02:00
top_navigate: str = None, bottom_navigate: str = None,
count=1):
2014-04-17 09:44:26 +02:00
"""Scroll the frame page-wise.
Args:
2014-08-03 00:33:39 +02:00
x: How many pages to scroll to the right.
y: How many pages to scroll down.
2015-05-31 15:02:09 +02:00
bottom_navigate: :navigate action (next, increment) to run when
scrolling down at the bottom of the page.
top_navigate: :navigate action (prev, decrement) to run when
scrolling up at the top of the page.
2014-04-17 09:44:26 +02:00
count: multiplier
"""
2016-06-14 17:32:36 +02:00
tab = self._current_widget()
if not tab.url().isValid():
2017-02-05 00:13:11 +01:00
# See https://github.com/qutebrowser/qutebrowser/issues/701
2015-05-29 23:49:48 +02:00
return
if bottom_navigate is not None and tab.scroller.at_bottom():
self.navigate(bottom_navigate)
return
elif top_navigate is not None and tab.scroller.at_top():
self.navigate(top_navigate)
return
2016-07-05 11:14:04 +02:00
try:
tab.scroller.delta_page(count * x, count * y)
2016-07-05 11:14:04 +02:00
except OverflowError:
raise cmdexc.CommandError(
"Numeric argument is too large for internal int "
"representation.")
2014-04-17 09:44:26 +02:00
2016-09-06 18:16:32 +02:00
def _yank_url(self, what):
"""Helper method for yank() to get the URL to copy."""
assert what in ['url', 'pretty-url'], what
flags = QUrl.RemovePassword
2016-09-22 14:25:48 +02:00
if what == 'pretty-url':
flags |= QUrl.DecodeReserved
else:
2016-09-06 18:16:32 +02:00
flags |= QUrl.FullyEncoded
url = QUrl(self._current_url())
url_query = QUrlQuery()
url_query_str = urlutils.query_string(url)
if '&' not in url_query_str and ';' in url_query_str:
url_query.setQueryDelimiters('=', ';')
url_query.setQuery(url_query_str)
2016-09-06 18:16:32 +02:00
for key in dict(url_query.queryItems()):
2017-07-02 12:07:27 +02:00
if key in config.val.url.yank_ignored_parameters:
2016-09-06 18:16:32 +02:00
url_query.removeQueryItem(key)
url.setQuery(url_query)
return url.toString(flags)
2014-09-28 22:13:14 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('what', choices=['selection', 'url', 'pretty-url',
'title', 'domain'])
def yank(self, what='url', sel=False, keep=False):
"""Yank something to the clipboard or primary selection.
2014-04-17 09:44:26 +02:00
2014-05-18 08:14:11 +02:00
Args:
what: What to yank.
2016-08-07 02:08:04 +02:00
- `url`: The current URL.
- `pretty-url`: The URL in pretty decoded form.
- `title`: The current page's title.
- `domain`: The current scheme, domain, and port number.
- `selection`: The selection under the cursor.
2014-09-13 00:22:27 +02:00
sel: Use the primary selection instead of the clipboard.
2016-08-07 02:08:04 +02:00
keep: Stay in visual mode after yanking the selection.
2014-05-18 08:14:11 +02:00
"""
if what == 'title':
s = self._tabbed_browser.page_title(self._current_index())
elif what == 'domain':
2015-06-04 13:20:39 +02:00
port = self._current_url().port()
s = '{}://{}{}'.format(self._current_url().scheme(),
self._current_url().host(),
':' + str(port) if port > -1 else '')
elif what in ['url', 'pretty-url']:
2016-09-06 18:16:32 +02:00
s = self._yank_url(what)
2016-08-07 02:08:04 +02:00
what = 'URL' # For printing
elif what == 'selection':
caret = self._current_widget().caret
s = caret.selection()
2016-08-07 02:15:51 +02:00
if not caret.has_selection() or not s:
2016-09-14 20:52:32 +02:00
message.info("Nothing to yank")
return
2016-08-07 02:15:51 +02:00
else: # pragma: no cover
raise ValueError("Invalid value {!r} for `what'.".format(what))
2016-05-08 22:06:00 +02:00
if sel and utils.supports_selection():
2014-05-19 11:56:51 +02:00
target = "primary selection"
else:
sel = False
2014-05-19 11:56:51 +02:00
target = "clipboard"
utils.set_clipboard(s, selection=sel)
if what != 'selection':
2016-09-14 20:52:32 +02:00
message.info("Yanked {} to {}: {}".format(what, target, s))
else:
2016-09-14 20:52:32 +02:00
message.info("{} {} yanked to {}".format(
len(s), "char" if len(s) == 1 else "chars", target))
if not keep:
2016-11-10 07:19:45 +01:00
modeman.leave(self._win_id, KeyMode.caret, "yank selected",
maybe=True)
2014-04-17 09:44:26 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def zoom_in(self, count=1):
"""Increase the zoom level for the current tab.
2014-04-17 09:44:26 +02:00
Args:
2014-08-03 00:33:39 +02:00
count: How many steps to zoom in.
2014-04-17 09:44:26 +02:00
"""
tab = self._current_widget()
try:
2016-06-15 13:02:24 +02:00
perc = tab.zoom.offset(count)
except ValueError as e:
raise cmdexc.CommandError(e)
2017-06-19 13:16:32 +02:00
message.info("Zoom level: {}%".format(int(perc)), replace=True)
2014-04-17 09:44:26 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def zoom_out(self, count=1):
"""Decrease the zoom level for the current tab.
2014-04-17 09:44:26 +02:00
Args:
2014-08-03 00:33:39 +02:00
count: How many steps to zoom out.
2014-04-17 09:44:26 +02:00
"""
tab = self._current_widget()
try:
2016-06-15 13:02:24 +02:00
perc = tab.zoom.offset(-count)
except ValueError as e:
raise cmdexc.CommandError(e)
2017-06-19 13:16:32 +02:00
message.info("Zoom level: {}%".format(int(perc)), replace=True)
2014-04-29 18:00:22 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
2017-06-26 21:51:35 +02:00
def zoom(self, zoom=None, count=None):
2014-08-03 00:33:39 +02:00
"""Set the zoom level for the current tab.
2016-08-02 17:29:35 +02:00
The zoom can be given as argument or as [count]. If neither is
given, the zoom is set to the default zoom. If both are given,
use [count].
2014-05-09 14:20:26 +02:00
Args:
2014-08-03 00:33:39 +02:00
zoom: The zoom percentage to set.
count: The zoom percentage to set.
2014-05-09 14:20:26 +02:00
"""
2017-06-26 21:51:35 +02:00
if zoom is not None:
try:
zoom = int(zoom.rstrip('%'))
except ValueError:
raise cmdexc.CommandError("zoom: Invalid int value {}"
.format(zoom))
2016-08-02 17:29:35 +02:00
level = count if count is not None else zoom
if level is None:
level = config.val.zoom.default
tab = self._current_widget()
try:
2016-06-15 13:02:24 +02:00
tab.zoom.set_factor(float(level) / 100)
except ValueError:
raise cmdexc.CommandError("Can't zoom {}%!".format(level))
2017-06-19 13:16:32 +02:00
message.info("Zoom level: {}%".format(int(level)), replace=True)
2014-05-09 14:20:26 +02:00
2014-09-28 22:13:14 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_only(self, prev=False, next_=False, force=False):
"""Close all tabs except for the current one.
Args:
prev: Keep tabs before the current.
next_: Keep tabs after the current.
force: Avoid confirmation for pinned tabs.
"""
cmdutils.check_exclusive((prev, next_), 'pn')
cur_idx = self._tabbed_browser.currentIndex()
assert cur_idx != -1
2017-05-13 01:56:07 +02:00
def _to_close(i):
"""Helper method to check if a tab should be closed or not."""
2017-05-13 01:56:07 +02:00
return not (i == cur_idx or
(prev and i < cur_idx) or
(next_ and i > cur_idx))
# Check to see if we are closing any pinned tabs
if not force:
for i, tab in enumerate(self._tabbed_browser.widgets()):
if _to_close(i) and tab.data.pinned:
self._tabbed_browser.tab_close_prompt_if_pinned(
tab,
force,
2017-05-13 01:56:07 +02:00
lambda: self.tab_only(
prev=prev, next_=next_, force=True))
return
first_tab = True
for i, tab in enumerate(self._tabbed_browser.widgets()):
if _to_close(i):
self._tabbed_browser.close_tab(tab, new_undo=first_tab)
first_tab = False
2014-05-17 22:38:07 +02:00
2014-09-28 22:13:14 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
2014-05-17 23:22:10 +02:00
def undo(self):
"""Re-open the last closed tab or tabs."""
try:
self._tabbed_browser.undo()
except IndexError:
2014-08-26 19:10:14 +02:00
raise cmdexc.CommandError("Nothing to undo!")
2014-05-17 22:38:07 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def tab_prev(self, count=1):
2014-08-03 00:33:39 +02:00
"""Switch to the previous tab, or switch [count] tabs back.
2014-05-17 22:38:07 +02:00
Args:
count: How many tabs to switch back.
"""
if self._count() == 0:
# Running :tab-prev after last tab was closed
2017-02-05 00:13:11 +01:00
# See https://github.com/qutebrowser/qutebrowser/issues/1448
return
newidx = self._current_index() - count
2014-05-17 22:38:07 +02:00
if newidx >= 0:
self._set_current_index(newidx)
elif config.val.tabs.wrap:
self._set_current_index(newidx % self._count())
2014-05-17 22:38:07 +02:00
else:
2014-08-26 19:10:14 +02:00
raise cmdexc.CommandError("First tab")
2014-05-17 22:38:07 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def tab_next(self, count=1):
2014-08-03 00:33:39 +02:00
"""Switch to the next tab, or switch [count] tabs forward.
2014-05-17 22:38:07 +02:00
Args:
count: How many tabs to switch forward.
"""
if self._count() == 0:
# Running :tab-next after last tab was closed
2017-02-05 00:13:11 +01:00
# See https://github.com/qutebrowser/qutebrowser/issues/1448
return
newidx = self._current_index() + count
if newidx < self._count():
self._set_current_index(newidx)
elif config.val.tabs.wrap:
self._set_current_index(newidx % self._count())
2014-05-17 22:38:07 +02:00
else:
2014-08-26 19:10:14 +02:00
raise cmdexc.CommandError("Last tab")
2014-05-17 22:38:07 +02:00
def _resolve_buffer_index(self, index):
2017-10-17 09:33:20 +02:00
"""Resolve a buffer index to the tabbedbrowser and tab.
Args:
index: The [win_id/]index of the tab to be selected. Or a substring
in which case the closest match will be focused.
"""
index_parts = index.split('/', 1)
try:
for part in index_parts:
int(part)
except ValueError:
model = miscmodels.buffer()
model.set_pattern(index)
if model.count() > 0:
index = model.data(model.first_item())
index_parts = index.split('/', 1)
else:
raise cmdexc.CommandError(
"No matching tab for: {}".format(index))
if len(index_parts) == 2:
win_id = int(index_parts[0])
idx = int(index_parts[1])
elif len(index_parts) == 1:
idx = int(index_parts[0])
active_win = objreg.get('app').activeWindow()
if active_win is None:
# Not sure how you enter a command without an active window...
raise cmdexc.CommandError(
"No window specified and couldn't find active window!")
win_id = active_win.win_id
if win_id not in objreg.window_registry:
raise cmdexc.CommandError(
"There's no window with id {}!".format(win_id))
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if not 0 < idx <= tabbed_browser.count():
raise cmdexc.CommandError(
"There's no tab with index {}!".format(idx))
return (tabbed_browser, tabbed_browser.widget(idx-1))
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
@cmdutils.argument('index', completion=miscmodels.buffer)
@cmdutils.argument('count', count=True)
def buffer(self, index=None, count=None):
"""Select tab by index or url/title best match.
Focuses window if necessary when index is given. If both index and
count are given, use count.
Args:
index: The [win_id/]index of the tab to focus. Or a substring
in which case the closest match will be focused.
count: The tab index to focus, starting with 1.
"""
if count is None and index is None:
raise cmdexc.CommandError("buffer: Either a count or the argument "
"index must be specified.")
if count is not None:
index = str(count)
tabbed_browser, tab = self._resolve_buffer_index(index)
window = tabbed_browser.window()
window.activateWindow()
window.raise_()
tabbed_browser.setCurrentWidget(tab)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['last'])
@cmdutils.argument('count', count=True)
def tab_focus(self, index: typing.Union[str, int] = None, count=None):
2014-05-17 22:38:07 +02:00
"""Select the tab given as argument/[count].
2015-08-11 22:01:18 +02:00
If neither count nor index are given, it behaves like tab-next.
2016-08-02 17:50:21 +02:00
If both are given, use count.
2015-08-11 22:01:18 +02:00
2014-05-17 22:38:07 +02:00
Args:
2014-08-03 00:39:39 +02:00
index: The tab index to focus, starting with 1. The special value
2016-08-02 17:50:21 +02:00
`last` focuses the last focused tab (regardless of count).
Negative indices count from the end, such that -1 is the
last tab.
2014-08-03 00:33:39 +02:00
count: The tab index to focus, starting with 1.
2014-05-17 22:38:07 +02:00
"""
index = count if count is not None else index
if index == 'last':
2014-08-03 00:39:39 +02:00
self._tab_focus_last()
return
elif index == self._current_index() + 1:
self._tab_focus_last(show_error=False)
return
elif index is None:
self.tab_next()
return
if index < 0:
index = self._count() + index + 1
2016-08-02 17:29:35 +02:00
if 1 <= index <= self._count():
self._set_current_index(index - 1)
2014-05-17 22:38:07 +02:00
else:
2014-08-26 19:10:14 +02:00
raise cmdexc.CommandError("There's no tab with index {}!".format(
2016-08-02 17:29:35 +02:00
index))
2014-05-17 22:38:07 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['+', '-'])
@cmdutils.argument('count', count=True)
def tab_move(self, index: typing.Union[str, int] = None, count=None):
"""Move the current tab according to the argument and [count].
If neither is given, move it to the first position.
2014-05-17 22:38:07 +02:00
Args:
index: `+` or `-` to move relative to the current tab by
count, or a default of 1 space.
A tab index to move to that index.
count: If moving relatively: Offset.
If moving absolutely: New position (default: 0). This
overrides the index argument, if given.
2014-05-17 22:38:07 +02:00
"""
if index in ['+', '-']:
2015-11-26 17:44:34 +01:00
# relative moving
new_idx = self._current_index()
2015-11-26 17:44:34 +01:00
delta = 1 if count is None else count
if index == '-':
new_idx -= delta
elif index == '+': # pragma: no branch
new_idx += delta
2015-12-02 06:20:41 +01:00
if config.val.tabs.wrap:
2015-12-02 06:20:41 +01:00
new_idx %= self._count()
else:
# absolute moving
if count is not None:
new_idx = count - 1
elif index is not None:
new_idx = index - 1 if index >= 0 else index + self._count()
else:
new_idx = 0
2015-11-26 17:44:34 +01:00
if not 0 <= new_idx < self._count():
2014-08-26 19:10:14 +02:00
raise cmdexc.CommandError("Can't move tab to position {}!".format(
new_idx + 1))
2015-11-26 17:44:34 +01:00
cur_idx = self._current_index()
2014-05-17 22:38:07 +02:00
cmdutils.check_overflow(cur_idx, 'int')
cmdutils.check_overflow(new_idx, 'int')
self._tabbed_browser.tabBar().moveTab(cur_idx, new_idx)
2014-05-17 22:38:07 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_replace_variables=True)
def spawn(self, cmdline, userscript=False, verbose=False, detach=False):
"""Spawn a command in a shell.
2014-05-03 14:25:22 +02:00
Args:
2016-03-25 11:09:03 +01:00
userscript: Run the command as a userscript. You can use an
absolute path, or store the userscript in one of those
locations:
- `~/.local/share/qutebrowser/userscripts`
(or `$XDG_DATA_DIR`)
- `/usr/share/qutebrowser/userscripts`
verbose: Show notifications when the command started/exited.
2015-05-31 16:25:25 +02:00
detach: Whether the command should be detached from qutebrowser.
cmdline: The commandline to execute.
2014-05-03 14:25:22 +02:00
"""
2017-07-23 21:11:33 +02:00
cmdutils.check_exclusive((userscript, detach), 'ud')
try:
cmd, *args = shlex.split(cmdline)
except ValueError as e:
raise cmdexc.CommandError("Error while splitting command: "
"{}".format(e))
args = runners.replace_variables(self._win_id, args)
log.procs.debug("Executing {} with args {}, userscript={}".format(
cmd, args, userscript))
if userscript:
2015-08-27 20:28:16 +02:00
# ~ expansion is handled by the userscript module.
self._run_userscript(cmd, *args, verbose=verbose)
else:
2015-08-27 20:28:16 +02:00
cmd = os.path.expanduser(cmd)
2016-09-14 20:52:32 +02:00
proc = guiprocess.GUIProcess(what='command', verbose=verbose,
parent=self._tabbed_browser)
2015-05-31 16:25:25 +02:00
if detach:
proc.start_detached(cmd, args)
2015-05-31 16:25:25 +02:00
else:
proc.start(cmd, args)
2014-09-28 22:13:14 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
2014-05-09 13:09:37 +02:00
def home(self):
"""Open main startpage in current tab."""
self.openurl(config.val.url.start_pages[0])
2014-05-09 13:09:37 +02:00
def _run_userscript(self, cmd, *args, verbose=False):
2015-06-29 17:48:30 +02:00
"""Run a userscript given as argument.
2014-07-29 01:45:42 +02:00
Args:
cmd: The userscript to run.
args: Arguments to pass to the userscript.
verbose: Show notifications when the command started/exited.
2014-07-29 01:45:42 +02:00
"""
env = {
'QUTE_MODE': 'command',
}
idx = self._current_index()
if idx != -1:
env['QUTE_TITLE'] = self._tabbed_browser.page_title(idx)
tab = self._tabbed_browser.currentWidget()
2016-07-05 20:11:42 +02:00
if tab is not None and tab.caret.has_selection():
env['QUTE_SELECTED_TEXT'] = tab.caret.selection()
try:
env['QUTE_SELECTED_HTML'] = tab.caret.selection(html=True)
except browsertab.UnsupportedOperationError:
pass
2016-07-05 20:11:42 +02:00
2016-07-07 18:03:37 +02:00
# FIXME:qtwebengine: If tab is None, run_async will fail!
try:
url = self._tabbed_browser.current_url()
except qtutils.QtValueError:
pass
else:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
try:
userscripts.run_async(tab, cmd, *args, win_id=self._win_id,
env=env, verbose=verbose)
2016-09-30 09:26:43 +02:00
except userscripts.Error as e:
raise cmdexc.CommandError(e)
2014-05-21 19:53:58 +02:00
2014-09-28 22:13:14 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
2014-05-22 16:44:47 +02:00
def quickmark_save(self):
"""Save the current page as a quickmark."""
quickmark_manager = objreg.get('quickmark-manager')
quickmark_manager.prompt_save(self._current_url())
2014-05-22 16:44:47 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window',
2016-05-10 23:04:52 +02:00
maxsplit=0)
@cmdutils.argument('name', completion=miscmodels.quickmark)
def quickmark_load(self, name, tab=False, bg=False, window=False):
2014-09-07 21:03:20 +02:00
"""Load a quickmark.
Args:
name: The name of the quickmark to load.
2014-09-13 00:22:27 +02:00
tab: Load the quickmark in a new tab.
bg: Load the quickmark in a new background tab.
window: Load the quickmark in a new window.
2014-09-07 21:03:20 +02:00
"""
try:
url = objreg.get('quickmark-manager').get(name)
except urlmarks.Error as e:
raise cmdexc.CommandError(str(e))
self._open(url, tab, bg, window)
2014-05-22 16:44:47 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
@cmdutils.argument('name', completion=miscmodels.quickmark)
def quickmark_del(self, name=None):
"""Delete a quickmark.
Args:
2016-07-26 08:36:16 +02:00
name: The name of the quickmark to delete. If not given, delete the
quickmark for the current page (choosing one arbitrarily
if there are more than one).
"""
quickmark_manager = objreg.get('quickmark-manager')
if name is None:
url = self._current_url()
try:
name = quickmark_manager.get_by_qurl(url)
except urlmarks.DoesNotExistError as e:
raise cmdexc.CommandError(str(e))
try:
quickmark_manager.delete(name)
except KeyError:
raise cmdexc.CommandError("Quickmark '{}' not found!".format(name))
2015-05-22 00:17:22 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
def bookmark_add(self, url=None, title=None, toggle=False):
2016-07-08 16:38:58 +02:00
"""Save the current page as a bookmark, or a specific url.
If no url and title are provided, then save the current page as a
bookmark.
If a url and title have been provided, then save the given url as
a bookmark with the provided title.
You can view all saved bookmarks on the
link:qute://bookmarks[bookmarks page].
2016-07-08 16:38:58 +02:00
Args:
url: url to save as a bookmark. If None, use url of current page.
2016-07-08 16:58:47 +02:00
title: title of the new bookmark.
toggle: remove the bookmark instead of raising an error if it
already exists.
2016-07-08 16:58:47 +02:00
"""
2016-07-07 21:16:19 +02:00
if url and not title:
raise cmdexc.CommandError('Title must be provided if url has '
2016-07-08 16:33:13 +02:00
'been provided')
2015-05-22 00:17:22 +02:00
bookmark_manager = objreg.get('bookmark-manager')
2016-07-08 16:33:13 +02:00
if url is None:
url = self._current_url()
else:
2016-07-07 21:16:19 +02:00
try:
url = urlutils.fuzzy_url(url)
except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e)
if not title:
title = self._current_title()
try:
2016-07-26 15:48:35 +02:00
was_added = bookmark_manager.add(url, title, toggle=toggle)
except urlmarks.Error as e:
raise cmdexc.CommandError(str(e))
else:
msg = "Bookmarked {}" if was_added else "Removed bookmark {}"
2016-09-14 20:52:32 +02:00
message.info(msg.format(url.toDisplayString()))
2015-05-22 00:17:22 +02:00
2015-05-25 01:07:57 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window',
2016-05-10 23:04:52 +02:00
maxsplit=0)
@cmdutils.argument('url', completion=miscmodels.bookmark)
2016-07-23 05:18:14 +02:00
def bookmark_load(self, url, tab=False, bg=False, window=False,
delete=False):
2015-05-25 01:07:57 +02:00
"""Load a bookmark.
Args:
url: The url of the bookmark to load.
tab: Load the bookmark in a new tab.
bg: Load the bookmark in a new background tab.
window: Load the bookmark in a new window.
2016-07-23 05:18:14 +02:00
delete: Whether to delete the bookmark afterwards.
2015-05-25 01:07:57 +02:00
"""
try:
2016-07-23 05:18:14 +02:00
qurl = urlutils.fuzzy_url(url)
except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e)
2016-07-23 05:18:14 +02:00
self._open(qurl, tab, bg, window)
if delete:
self.bookmark_del(url)
2015-05-25 01:07:57 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
@cmdutils.argument('url', completion=miscmodels.bookmark)
def bookmark_del(self, url=None):
"""Delete a bookmark.
Args:
2016-07-26 08:36:16 +02:00
url: The url of the bookmark to delete. If not given, use the
current page's url.
"""
if url is None:
2016-08-09 17:28:14 +02:00
url = self._current_url().toString(QUrl.RemovePassword |
QUrl.FullyEncoded)
try:
objreg.get('bookmark-manager').delete(url)
except KeyError:
raise cmdexc.CommandError("Bookmark '{}' not found!".format(url))
@cmdutils.register(instance='command-dispatcher', scope='window')
2016-07-05 20:11:42 +02:00
def follow_selected(self, *, tab=False):
"""Follow the selected text.
Args:
tab: Load the selected link in a new tab.
"""
2016-07-04 13:35:38 +02:00
try:
self._current_widget().caret.follow_selected(tab=tab)
2016-07-10 17:23:08 +02:00
except browsertab.WebTabError as e:
2016-07-04 13:35:38 +02:00
raise cmdexc.CommandError(str(e))
2015-05-30 16:37:25 +02:00
2014-09-28 22:13:14 +02:00
@cmdutils.register(instance='command-dispatcher', name='inspector',
scope='window')
def toggle_inspector(self):
"""Toggle the web inspector.
Note: Due a bug in Qt, the inspector will show incorrect request
headers in the network tab.
"""
2016-07-04 17:49:25 +02:00
tab = self._current_widget()
# FIXME:qtwebengine have a proper API for this
page = tab._widget.page() # pylint: disable=protected-access
try:
if tab.data.inspector is None:
tab.data.inspector = inspector.create()
tab.data.inspector.inspect(page)
else:
tab.data.inspector.toggle(page)
except inspector.WebInspectorError as e:
raise cmdexc.CommandError(e)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('dest_old', hide=True)
def download(self, url=None, dest_old=None, *, mhtml_=False, dest=None):
2015-02-09 12:06:49 +01:00
"""Download a given URL, or current page if no URL given.
The form `:download [url] [dest]` is deprecated, use `:download --dest
[dest] [url]` instead.
Args:
2015-02-09 17:38:50 +01:00
url: The URL to download. If not given, download the current page.
dest_old: (deprecated) Same as dest.
dest: The file path to write the download to, or None to ask.
2015-10-07 23:53:03 +02:00
mhtml_: Download the current page and all assets as mhtml file.
"""
if dest_old is not None:
2016-09-14 20:52:32 +02:00
message.warning(":download [url] [dest] is deprecated - use "
":download --dest [dest] [url]")
if dest is not None:
raise cmdexc.CommandError("Can't give two destinations for the"
" download.")
dest = dest_old
2016-11-01 17:50:29 +01:00
# FIXME:qtwebengine do this with the QtWebEngine download manager?
download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window=self._win_id)
target = None
if dest is not None:
dest = downloads.transform_path(dest)
if dest is None:
raise cmdexc.CommandError("Invalid target filename")
target = downloads.FileDownloadTarget(dest)
tab = self._current_widget()
user_agent = tab.user_agent()
2015-02-09 12:06:49 +01:00
if url:
2015-10-07 23:53:03 +02:00
if mhtml_:
raise cmdexc.CommandError("Can only download the current page"
" as mhtml.")
url = urlutils.qurl_from_user_input(url)
urlutils.raise_cmdexc_if_invalid(url)
download_manager.get(url, user_agent=user_agent, target=target)
2016-07-07 11:47:02 +02:00
elif mhtml_:
2017-02-07 00:15:39 +01:00
tab = self._current_widget()
if tab.backend == usertypes.Backend.QtWebEngine:
webengine_download_manager = objreg.get(
'webengine-download-manager')
try:
webengine_download_manager.get_mhtml(tab, target)
except browsertab.UnsupportedOperationError as e:
raise cmdexc.CommandError(e)
else:
download_manager.get_mhtml(tab, target)
else:
qnam = tab.networkaccessmanager()
suggested_fn = downloads.suggested_fn_from_title(
self._current_url().path(), tab.title()
)
download_manager.get(
self._current_url(),
user_agent=user_agent,
qnam=qnam,
target=target,
suggested_fn=suggested_fn
)
2014-06-19 17:58:46 +02:00
2014-09-28 22:13:14 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
2014-09-15 17:59:54 +02:00
def view_source(self):
"""Show the source of the current page in a new tab."""
2016-07-04 16:34:07 +02:00
tab = self._current_widget()
if tab.data.viewing_source:
2014-09-15 17:59:54 +02:00
raise cmdexc.CommandError("Already viewing source!")
2016-07-04 16:34:07 +02:00
try:
current_url = self._current_url()
except cmdexc.CommandError as e:
message.error(str(e))
return
2016-07-04 16:34:07 +02:00
def show_source_cb(source):
2016-07-05 20:11:42 +02:00
"""Show source as soon as it's ready."""
2017-10-22 22:16:02 +02:00
# WORKAROUND for https://github.com/PyCQA/pylint/issues/491
# pylint: disable=no-member
2016-07-04 16:34:07 +02:00
lexer = pygments.lexers.HtmlLexer()
2017-05-15 09:16:24 +02:00
formatter = pygments.formatters.HtmlFormatter(
full=True, linenos='table',
title='Source for {}'.format(current_url.toDisplayString()))
2017-10-22 22:16:02 +02:00
# pylint: enable=no-member
2016-07-04 16:34:07 +02:00
highlighted = pygments.highlight(source, lexer, formatter)
2017-05-15 09:16:24 +02:00
new_tab = self._tabbed_browser.tabopen()
new_tab.set_html(highlighted)
2016-07-04 16:34:07 +02:00
new_tab.data.viewing_source = True
tab.dump_async(show_source_cb)
2014-09-15 17:59:54 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window',
debug=True)
def debug_dump_page(self, dest, plain=False):
"""Dump the current page's content to a file.
Args:
dest: Where to write the file to.
plain: Write plain text instead of HTML.
"""
2016-06-14 13:26:30 +02:00
tab = self._current_widget()
2015-11-24 18:04:10 +01:00
dest = os.path.expanduser(dest)
2016-06-14 13:26:30 +02:00
def callback(data):
try:
with open(dest, 'w', encoding='utf-8') as f:
f.write(data)
except OSError as e:
2016-09-14 20:52:32 +02:00
message.error('Could not write page: {}'.format(e))
2016-06-14 13:26:30 +02:00
else:
2016-09-14 20:52:32 +02:00
message.info("Dumped page to {}.".format(dest))
2016-06-14 13:26:30 +02:00
tab.dump_async(callback, plain=plain)
2017-02-11 17:22:35 +01:00
@cmdutils.register(instance='command-dispatcher', scope='window')
def history(self, tab=True, bg=False, window=False):
"""Show browsing history.
2017-02-10 13:47:20 +01:00
Args:
tab: Open in a new tab.
bg: Open in a background tab.
window: Open in a new window.
"""
url = QUrl('qute://history/')
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', name='help',
2014-09-28 22:13:14 +02:00
scope='window')
@cmdutils.argument('topic', completion=miscmodels.helptopic)
2014-10-06 20:40:00 +02:00
def show_help(self, tab=False, bg=False, window=False, topic=None):
2014-09-08 07:44:32 +02:00
r"""Show help about a command or setting.
2014-09-08 06:57:22 +02:00
Args:
2014-10-06 20:40:00 +02:00
tab: Open in a new tab.
bg: Open in a background tab.
window: Open in a new window.
2014-09-08 06:57:22 +02:00
topic: The topic to show help for.
- :__command__ for commands.
- __section__.__option__ for settings.
2014-09-08 06:57:22 +02:00
"""
2014-09-08 12:18:54 +02:00
if topic is None:
path = 'index.html'
elif topic.startswith(':'):
2014-09-08 06:57:22 +02:00
command = topic[1:]
if command not in cmdutils.cmd_dict:
raise cmdexc.CommandError("Invalid command {}!".format(
command))
path = 'commands.html#{}'.format(command)
2017-06-19 12:09:09 +02:00
elif topic in configdata.DATA:
path = 'settings.html#{}'.format(topic)
2014-09-08 06:57:22 +02:00
else:
raise cmdexc.CommandError("Invalid help topic {}!".format(topic))
2014-10-06 20:40:00 +02:00
url = QUrl('qute://help/{}'.format(path))
self._open(url, tab, bg, window)
2014-09-08 06:57:22 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
2017-06-08 12:35:21 +02:00
def messages(self, level='info', plain=False, tab=False, bg=False,
window=False):
"""Show a log of past messages.
Args:
level: Include messages with `level` or higher severity.
Valid values: vdebug, debug, info, warning, error, critical.
plain: Whether to show plaintext (as opposed to html).
tab: Open in a new tab.
bg: Open in a background tab.
window: Open in a new window.
"""
if level.upper() not in log.LOG_LEVELS:
raise cmdexc.CommandError("Invalid log level {}!".format(level))
if plain:
url = QUrl('qute://plainlog?level={}'.format(level))
else:
url = QUrl('qute://log?level={}'.format(level))
self._open(url, tab, bg, window)
def _open_editor_cb(self, elem):
"""Open editor after the focus elem was found in open_editor."""
if elem is None:
2016-09-14 20:52:32 +02:00
message.error("No element focused!")
return
if not elem.is_editable(strict=True):
2016-09-14 20:52:32 +02:00
message.error("Focused element is not editable!")
return
text = elem.value()
if text is None:
message.error("Could not get text from the focused element.")
return
2017-04-28 15:15:32 +02:00
assert isinstance(text, str), text
caret_position = elem.caret_position()
2016-09-14 20:52:32 +02:00
ed = editor.ExternalEditor(self._tabbed_browser)
ed.editing_finished.connect(functools.partial(
self.on_editing_finished, elem))
ed.edit(text, caret_position)
@cmdutils.register(instance='command-dispatcher', scope='window')
2014-05-17 23:22:10 +02:00
def open_editor(self):
2014-08-03 00:33:39 +02:00
"""Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the
`editor.command` config option.
"""
2016-07-05 20:11:42 +02:00
tab = self._current_widget()
2016-08-18 14:01:27 +02:00
tab.elements.find_focused(self._open_editor_cb)
def on_editing_finished(self, elem, text):
2014-04-30 10:46:20 +02:00
"""Write the editor text into the form field and clean up tempfile.
2014-04-29 18:00:22 +02:00
Callback for GUIProcess when the editor was closed.
Args:
2014-09-04 08:00:05 +02:00
elem: The WebElementWrapper which was modified.
text: The new text to insert.
2014-04-29 18:00:22 +02:00
"""
2014-09-04 08:00:05 +02:00
try:
elem.set_value(text)
except webelem.Error as e:
raise cmdexc.CommandError(str(e))
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
2016-09-07 11:20:32 +02:00
scope='window')
def insert_text(self, text):
"""Insert text at cursor position.
Args:
2016-08-10 22:29:19 +02:00
text: The text to insert.
"""
2016-07-05 20:11:42 +02:00
tab = self._current_widget()
2016-09-07 11:20:32 +02:00
def _insert_text_cb(elem):
if elem is None:
2016-09-14 20:52:32 +02:00
message.error("No element focused!")
2016-09-07 11:20:32 +02:00
return
try:
elem.insert_text(text)
except webelem.Error as e:
2016-09-14 20:52:32 +02:00
message.error(str(e))
2016-09-07 11:20:32 +02:00
return
tab.elements.find_focused(_insert_text_cb)
@cmdutils.register(instance='command-dispatcher', scope='window')
2016-08-18 15:30:04 +02:00
@cmdutils.argument('filter_', choices=['id'])
def click_element(self, filter_: str, value, *,
target: usertypes.ClickTarget =
usertypes.ClickTarget.normal,
force_event=False):
2016-08-18 15:30:04 +02:00
"""Click the element matching the given filter.
The given filter needs to result in exactly one element, otherwise, an
error is shown.
Args:
2016-08-18 16:53:28 +02:00
filter_: How to filter the elements.
id: Get an element based on its ID.
2016-08-18 15:30:04 +02:00
value: The value to filter for.
2016-08-18 16:53:28 +02:00
target: How to open the clicked element (normal/tab/tab-bg/window).
force_event: Force generating a fake click event.
2016-08-18 15:30:04 +02:00
"""
tab = self._current_widget()
def single_cb(elem):
"""Click a single element."""
if elem is None:
2016-11-15 19:43:03 +01:00
message.error("No element found with id {}!".format(value))
2016-08-18 15:30:04 +02:00
return
try:
elem.click(target, force_event=force_event)
except webelem.Error as e:
2016-09-14 20:52:32 +02:00
message.error(str(e))
return
2016-08-18 15:30:04 +02:00
# def multiple_cb(elems):
# """Click multiple elements (with only one expected)."""
# if not elems:
2016-09-14 20:52:32 +02:00
# message.error("No element found!")
2016-08-18 15:30:04 +02:00
# return
# elif len(elems) != 1:
2016-09-14 20:52:32 +02:00
# message.error("{} elements found!".format(len(elems)))
2016-08-18 15:30:04 +02:00
# return
# elems[0].click(target)
handlers = {
'id': (tab.elements.find_id, single_cb),
}
handler, callback = handlers[filter_]
handler(value, callback)
def _search_cb(self, found, *, tab, old_scroll_pos, options, text, prev):
"""Callback called from search/search_next/search_prev.
Args:
found: Whether the text was found.
tab: The AbstractTab in which the search was made.
old_scroll_pos: The scroll position (QPoint) before the search.
options: The options (dict) the search was made with.
text: The text searched for.
prev: Whether we're searching backwards (i.e. :search-prev)
"""
# :search/:search-next without reverse -> down
# :search/:search-next with reverse -> up
# :search-prev without reverse -> up
# :search-prev with reverse -> down
going_up = options['reverse'] ^ prev
if found:
# Check if the scroll position got smaller and show info.
if not going_up and tab.scroller.pos_px().y() < old_scroll_pos.y():
2016-09-14 20:52:32 +02:00
message.info("Search hit BOTTOM, continuing at TOP")
elif going_up and tab.scroller.pos_px().y() > old_scroll_pos.y():
2016-09-14 20:52:32 +02:00
message.info("Search hit TOP, continuing at BOTTOM")
else:
2016-09-14 20:52:32 +02:00
message.warning("Text '{}' not found on page!".format(text))
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
def search(self, text="", reverse=False):
"""Search for a text on the current page. With no text, clear results.
Args:
text: The text to search for.
reverse: Reverse search direction.
"""
self.set_mark("'")
2016-07-04 11:23:46 +02:00
tab = self._current_widget()
if tab.search.search_displayed:
tab.search.clear()
2016-07-04 11:23:46 +02:00
if not text:
return
2016-07-04 11:23:46 +02:00
options = {
'ignore_case': config.val.ignore_case,
2016-07-04 11:23:46 +02:00
'reverse': reverse,
}
self._tabbed_browser.search_text = text
self._tabbed_browser.search_options = dict(options)
cb = functools.partial(self._search_cb, tab=tab,
2017-03-20 22:06:05 +01:00
old_scroll_pos=tab.scroller.pos_px(),
options=options, text=text, prev=False)
options['result_cb'] = cb
tab.search.search(text, **options)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def search_next(self, count=1):
"""Continue the search to the ([count]th) next term.
Args:
count: How many elements to ignore.
"""
2016-07-04 11:23:46 +02:00
tab = self._current_widget()
window_text = self._tabbed_browser.search_text
window_options = self._tabbed_browser.search_options
if window_text is None:
raise cmdexc.CommandError("No search done yet.")
self.set_mark("'")
2016-07-04 11:23:46 +02:00
if window_text is not None and window_text != tab.search.text:
tab.search.clear()
tab.search.search(window_text, **window_options)
count -= 1
if count == 0:
return
cb = functools.partial(self._search_cb, tab=tab,
old_scroll_pos=tab.scroller.pos_px(),
options=window_options, text=window_text,
prev=False)
for _ in range(count - 1):
2016-07-04 11:23:46 +02:00
tab.search.next_result()
tab.search.next_result(result_cb=cb)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def search_prev(self, count=1):
"""Continue the search to the ([count]th) previous term.
Args:
count: How many elements to ignore.
"""
2016-07-04 11:23:46 +02:00
tab = self._current_widget()
window_text = self._tabbed_browser.search_text
window_options = self._tabbed_browser.search_options
if window_text is None:
raise cmdexc.CommandError("No search done yet.")
self.set_mark("'")
2016-07-04 11:23:46 +02:00
if window_text is not None and window_text != tab.search.text:
tab.search.clear()
tab.search.search(window_text, **window_options)
count -= 1
if count == 0:
return
cb = functools.partial(self._search_cb, tab=tab,
old_scroll_pos=tab.scroller.pos_px(),
options=window_options, text=window_text,
prev=True)
for _ in range(count - 1):
2016-07-04 11:23:46 +02:00
tab.search.prev_result()
tab.search.prev_result(result_cb=cb)
2015-04-22 07:13:56 +02:00
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
@cmdutils.argument('count', count=True)
def move_to_next_line(self, count=1):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the next line.
Args:
count: How many lines to move.
"""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_next_line(count)
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
@cmdutils.argument('count', count=True)
def move_to_prev_line(self, count=1):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the prev line.
Args:
count: How many lines to move.
"""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_prev_line(count)
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
@cmdutils.argument('count', count=True)
def move_to_next_char(self, count=1):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the next char.
Args:
count: How many lines to move.
"""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_next_char(count)
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
@cmdutils.argument('count', count=True)
def move_to_prev_char(self, count=1):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the previous char.
Args:
count: How many chars to move.
"""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_prev_char(count)
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
@cmdutils.argument('count', count=True)
def move_to_end_of_word(self, count=1):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the end of the word.
Args:
count: How many words to move.
"""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_end_of_word(count)
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
@cmdutils.argument('count', count=True)
def move_to_next_word(self, count=1):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the next word.
Args:
count: How many words to move.
"""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_next_word(count)
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
@cmdutils.argument('count', count=True)
def move_to_prev_word(self, count=1):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the previous word.
Args:
count: How many words to move.
"""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_prev_word(count)
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
2015-05-18 22:28:11 +02:00
def move_to_start_of_line(self):
2015-05-18 22:27:44 +02:00
"""Move the cursor or selection to the start of the line."""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_start_of_line()
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
2015-05-18 22:28:11 +02:00
def move_to_end_of_line(self):
2015-05-18 22:27:44 +02:00
"""Move the cursor or selection to the end of line."""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_end_of_line()
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
@cmdutils.argument('count', count=True)
def move_to_start_of_next_block(self, count=1):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the start of next block.
Args:
count: How many blocks to move.
"""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_start_of_next_block(count)
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
@cmdutils.argument('count', count=True)
def move_to_start_of_prev_block(self, count=1):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the start of previous block.
Args:
count: How many blocks to move.
"""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_start_of_prev_block(count)
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
@cmdutils.argument('count', count=True)
def move_to_end_of_next_block(self, count=1):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the end of next block.
Args:
count: How many blocks to move.
"""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_end_of_next_block(count)
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
@cmdutils.argument('count', count=True)
def move_to_end_of_prev_block(self, count=1):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the end of previous block.
Args:
count: How many blocks to move.
"""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_end_of_prev_block(count)
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
def move_to_start_of_document(self):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the start of the document."""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_start_of_document()
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
def move_to_end_of_document(self):
2015-05-11 22:29:44 +02:00
"""Move the cursor or selection to the end of the document."""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.move_to_end_of_document()
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
def toggle_selection(self):
"""Toggle caret selection mode."""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.toggle_selection()
@cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret],
scope='window')
def drop_selection(self):
"""Drop selection and keep selection mode enabled."""
2016-06-14 18:08:46 +02:00
self._current_widget().caret.drop_selection()
2015-04-22 07:13:56 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window',
debug=True)
@cmdutils.argument('count', count=True)
2015-04-22 07:13:56 +02:00
def debug_webaction(self, action, count=1):
"""Execute a webaction.
2017-05-09 22:02:30 +02:00
Available actions:
http://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit)
http://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine)
2015-04-22 07:13:56 +02:00
Args:
action: The action to execute, e.g. MoveToNextChar.
count: How many times to repeat the action.
"""
2016-07-04 15:45:20 +02:00
tab = self._current_widget()
2015-04-22 07:13:56 +02:00
for _ in range(count):
2017-05-09 22:02:30 +02:00
try:
tab.action.run_string(action)
except browsertab.WebTabError as e:
raise cmdexc.CommandError(str(e))
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_cmd_split=True)
2017-02-24 19:07:30 +01:00
def jseval(self, js_code, file=False, quiet=False, *,
world: typing.Union[usertypes.JsWorld, int] = None):
"""Evaluate a JavaScript string.
Args:
2017-02-24 19:07:30 +01:00
js_code: The string/file to evaluate.
file: Interpret js-code as a path to a file.
2015-06-12 11:24:04 +02:00
quiet: Don't show resulting JS object.
2016-09-12 18:23:23 +02:00
world: Ignored on QtWebKit. On QtWebEngine, a world ID or name to
run the snippet in.
"""
2016-09-12 18:27:51 +02:00
if world is None:
world = usertypes.JsWorld.jseval
2015-06-12 11:24:04 +02:00
if quiet:
2016-06-14 18:47:26 +02:00
jseval_cb = None
else:
2016-06-14 18:47:26 +02:00
def jseval_cb(out):
if out is None:
# Getting the actual error (if any) seems to be difficult.
# The error does end up in
2016-07-05 20:11:42 +02:00
# BrowserPage.javaScriptConsoleMessage(), but
# distinguishing between :jseval errors and errors from the
# webpage is not trivial...
2016-09-14 20:52:32 +02:00
message.info('No output or error')
2016-06-14 18:47:26 +02:00
else:
# The output can be a string, number, dict, array, etc. But
# *don't* output too much data, as this will make
# qutebrowser hang
out = str(out)
if len(out) > 5000:
2016-07-05 20:11:42 +02:00
out = out[:5000] + ' [...trimmed...]'
2016-09-14 20:52:32 +02:00
message.info(out)
2016-06-14 18:47:26 +02:00
2017-02-24 19:07:30 +01:00
if file:
2017-08-13 02:34:50 +02:00
path = os.path.expanduser(js_code)
2017-02-24 19:07:30 +01:00
try:
2017-08-13 02:34:50 +02:00
with open(path, 'r', encoding='utf-8') as f:
2017-02-24 19:07:30 +01:00
js_code = f.read()
except OSError as e:
raise cmdexc.CommandError(str(e))
2016-09-12 15:59:17 +02:00
widget = self._current_widget()
2016-09-12 18:27:51 +02:00
widget.run_js_async(js_code, callback=jseval_cb, world=world)
2016-06-14 18:47:26 +02:00
@cmdutils.register(instance='command-dispatcher', scope='window')
def fake_key(self, keystring, global_=False):
"""Send a fake keypress or key string to the website or qutebrowser.
:fake-key xy - sends the keychain 'xy'
:fake-key <Ctrl-x> - sends Ctrl-x
:fake-key <Escape> - sends the escape key
Args:
keystring: The keystring to send.
global_: If given, the keys are sent to the qutebrowser UI.
"""
try:
keyinfos = utils.parse_keystring(keystring)
except utils.KeyParseError as e:
raise cmdexc.CommandError(str(e))
for keyinfo in keyinfos:
press_event = QKeyEvent(QEvent.KeyPress, keyinfo.key,
keyinfo.modifiers, keyinfo.text)
release_event = QKeyEvent(QEvent.KeyRelease, keyinfo.key,
keyinfo.modifiers, keyinfo.text)
if global_:
2016-08-18 18:44:33 +02:00
window = QApplication.focusWindow()
if window is None:
raise cmdexc.CommandError("No focused window!")
QApplication.postEvent(window, press_event)
QApplication.postEvent(window, release_event)
else:
2016-08-18 18:44:33 +02:00
tab = self._current_widget()
tab.send_event(press_event)
tab.send_event(release_event)
2016-01-13 21:05:48 +01:00
@cmdutils.register(instance='command-dispatcher', scope='window',
debug=True, backend=usertypes.Backend.QtWebKit)
2016-01-13 21:05:48 +01:00
def debug_clear_ssl_errors(self):
"""Clear remembered SSL error answers."""
self._current_widget().clear_ssl_errors()
@cmdutils.register(instance='command-dispatcher', scope='window')
def edit_url(self, url=None, bg=False, tab=False, window=False,
2017-10-27 04:13:35 +02:00
private=False, related=False):
"""Navigate to a url formed in an external editor.
The editor which should be launched can be configured via the
`editor.command` config option.
Args:
url: URL to edit; defaults to the current page url.
bg: Open in a new background tab.
tab: Open in a new tab.
window: Open in a new window.
private: Open a new window in private browsing mode.
2017-10-27 04:13:35 +02:00
related: If opening a new tab, position the tab as related to the
current one (like clicking on a link).
"""
cmdutils.check_exclusive((tab, bg, window), 'tbw')
old_url = self._current_url().toString()
2016-09-14 20:52:32 +02:00
ed = editor.ExternalEditor(self._tabbed_browser)
# Passthrough for openurl args (e.g. -t, -b, -w)
2016-04-05 18:37:50 +02:00
ed.editing_finished.connect(functools.partial(
self._open_if_changed, old_url=old_url, bg=bg, tab=tab,
2017-10-27 04:13:35 +02:00
window=window, private=private, related=related))
ed.edit(url or old_url)
@cmdutils.register(instance='command-dispatcher', scope='window')
def set_mark(self, key):
2016-04-13 04:00:29 +02:00
"""Set a mark at the current scroll position in the current tab.
Args:
key: mark identifier; capital indicates a global mark
"""
self._tabbed_browser.set_mark(key)
@cmdutils.register(instance='command-dispatcher', scope='window')
def jump_mark(self, key):
2016-04-13 04:00:29 +02:00
"""Jump to the mark named by `key`.
Args:
key: mark identifier; capital indicates a global mark
"""
self._tabbed_browser.jump_mark(key)
def _open_if_changed(self, url=None, old_url=None, bg=False, tab=False,
2017-10-27 04:13:35 +02:00
window=False, private=False, related=False):
"""Open a URL unless it's already open in the tab.
Args:
old_url: The original URL to compare against.
url: The URL to open.
bg: Open in a new background tab.
tab: Open in a new tab.
window: Open in a new window.
private: Open a new window in private browsing mode.
2017-10-27 04:13:35 +02:00
related: If opening a new tab, position the tab as related to the
current one (like clicking on a link).
"""
2017-10-27 04:13:35 +02:00
if bg or tab or window or private or related or url != old_url:
self.openurl(url=url, bg=bg, tab=tab, window=window,
2017-10-27 04:13:35 +02:00
private=private, related=related)
2017-01-02 20:02:02 +01:00
@cmdutils.register(instance='command-dispatcher', scope='window')
def fullscreen(self, leave=False):
"""Toggle fullscreen mode.
Args:
leave: Only leave fullscreen if it was entered by the page.
"""
if leave:
tab = self._current_widget()
try:
tab.action.exit_fullscreen()
except browsertab.UnsupportedOperationError:
pass
return
window = self._tabbed_browser.window()
if window.isFullScreen():
2017-07-07 11:30:18 +02:00
window.setWindowState(
window.state_before_fullscreen & ~Qt.WindowFullScreen)
2017-01-02 20:02:02 +01:00
else:
window.state_before_fullscreen = window.windowState()
2017-01-02 20:02:02 +01:00
window.showFullScreen()
2017-07-07 11:30:18 +02:00
log.misc.debug('state before fullscreen: {}'.format(
debug.qflags_key(Qt, window.state_before_fullscreen)))