# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2018-2021 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # # qutebrowser is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # qutebrowser is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . """Various commands.""" import os import signal import functools import logging import pathlib from typing import Optional try: import hunter except ImportError: hunter = None from PyQt5.QtCore import Qt from PyQt5.QtPrintSupport import QPrintPreviewDialog from qutebrowser.api import cmdutils, apitypes, message, config # FIXME should be part of qutebrowser.api? from qutebrowser.completion.models import miscmodels from qutebrowser.utils import utils _LOGGER = logging.getLogger('misc') @cmdutils.register(name='reload') @cmdutils.argument('tab', value=cmdutils.Value.count_tab) def reloadpage(tab: Optional[apitypes.Tab], force: bool = False) -> None: """Reload the current/[count]th tab. Args: count: The tab index to reload, or None. force: Bypass the page cache. """ if tab is not None: tab.reload(force=force) @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.count_tab) def stop(tab: Optional[apitypes.Tab]) -> None: """Stop loading in the current/[count]th tab. Args: count: The tab index to stop, or None. """ if tab is not None: tab.stop() def _print_preview(tab: apitypes.Tab) -> None: """Show a print preview.""" def print_callback(ok: bool) -> None: if not ok: message.error("Printing failed!") tab.printing.check_preview_support() diag = QPrintPreviewDialog(tab) diag.setAttribute(Qt.WA_DeleteOnClose) diag.setWindowFlags( diag.windowFlags() | # type: ignore[operator, arg-type] Qt.WindowMaximizeButtonHint | Qt.WindowMinimizeButtonHint) diag.paintRequested.connect(functools.partial( tab.printing.to_printer, callback=print_callback)) diag.exec() def _print_pdf(tab: apitypes.Tab, filename: str) -> None: """Print to the given PDF file.""" 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) tab.printing.to_pdf(filename) _LOGGER.debug("Print to file: {}".format(filename)) @cmdutils.register(name='print') @cmdutils.argument('tab', value=cmdutils.Value.count_tab) @cmdutils.argument('pdf', flag='f', metavar='file') def printpage(tab: Optional[apitypes.Tab], preview: bool = False, *, pdf: str = None) -> None: """Print the current/[count]th tab. Args: preview: Show preview instead of printing. count: The tab index to print, or None. pdf: The file path to write the PDF to. """ if tab is None: return try: if preview: _print_preview(tab) elif pdf: _print_pdf(tab, pdf) else: tab.printing.show_dialog() except apitypes.WebTabError as e: raise cmdutils.CommandError(e) @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def home(tab: apitypes.Tab) -> None: """Open main startpage in current tab.""" if tab.navigation_blocked(): message.info("Tab is pinned!") else: tab.load_url(config.val.url.start_pages[0]) @cmdutils.register(debug=True) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def debug_dump_page(tab: apitypes.Tab, dest: str, plain: bool = False) -> None: """Dump the current page's content to a file. Args: dest: Where to write the file to. plain: Write plain text instead of HTML. """ dest = os.path.expanduser(dest) def callback(data: str) -> None: """Write the data to disk.""" try: with open(dest, 'w', encoding='utf-8') as f: f.write(data) except OSError as e: message.error('Could not write page: {}'.format(e)) else: message.info("Dumped page to {}.".format(dest)) tab.dump_async(callback, plain=plain) @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def screenshot( tab: apitypes.Tab, filename: pathlib.Path, *, rect: str = None, force: bool = False, ) -> None: """Take a screenshot of the currently shown part of the page. The file format is automatically determined based on the given file extension. Args: filename: The file to save the screenshot to (~ gets expanded). rect: The rectangle to save, as a string like WxH+X+Y. force: Overwrite existing files. """ expanded = filename.expanduser() if expanded.exists() and not force: raise cmdutils.CommandError( f"File {filename} already exists (use --force to overwrite)") qrect = None if rect is None else utils.parse_rect(rect) pic = tab.grab_pixmap(qrect) if pic is None: raise cmdutils.CommandError("Getting screenshot failed") ok = pic.save(str(expanded)) if not ok: raise cmdutils.CommandError(f"Saving to {filename} failed") _LOGGER.debug(f"Screenshot saved to {filename}") @cmdutils.register(maxsplit=0) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def insert_text(tab: apitypes.Tab, text: str) -> None: """Insert text at cursor position. Args: text: The text to insert. """ def _insert_text_cb(elem: Optional[apitypes.WebElement]) -> None: if elem is None: message.error("No element focused!") return try: elem.insert_text(text) except apitypes.WebElemError as e: message.error(str(e)) return tab.elements.find_focused(_insert_text_cb) @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('filter_', choices=['id']) def click_element(tab: apitypes.Tab, filter_: str, value: str, *, target: apitypes.ClickTarget = apitypes.ClickTarget.normal, force_event: bool = False) -> None: """Click the element matching the given filter. The given filter needs to result in exactly one element, otherwise, an error is shown. Args: filter_: How to filter the elements. id: Get an element based on its ID. value: The value to filter for. target: How to open the clicked element (normal/tab/tab-bg/window). force_event: Force generating a fake click event. """ def single_cb(elem: Optional[apitypes.WebElement]) -> None: """Click a single element.""" if elem is None: message.error("No element found with id {}!".format(value)) return try: elem.click(target, force_event=force_event) except apitypes.WebElemError as e: message.error(str(e)) return handlers = { 'id': (tab.elements.find_id, single_cb), } handler, callback = handlers[filter_] handler(value, callback) @cmdutils.register(debug=True) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def debug_webaction(tab: apitypes.Tab, action: str, count: int = 1) -> None: """Execute a webaction. Available actions: https://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit) https://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine) Args: action: The action to execute, e.g. MoveToNextChar. count: How many times to repeat the action. """ for _ in range(count): try: tab.action.run_string(action) except apitypes.WebTabError as e: raise cmdutils.CommandError(str(e)) @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.count_tab) def tab_mute(tab: Optional[apitypes.Tab]) -> None: """Mute/Unmute the current/[count]th tab. Args: count: The tab index to mute or unmute, or None """ if tab is None: return try: tab.audio.set_muted(not tab.audio.is_muted(), override=True) except apitypes.WebTabError as e: raise cmdutils.CommandError(e) @cmdutils.register() def nop() -> None: """Do nothing.""" @cmdutils.register() def message_error(text: str) -> None: """Show an error message in the statusbar. Args: text: The text to show. """ message.error(text) @cmdutils.register() @cmdutils.argument('count', value=cmdutils.Value.count) def message_info(text: str, count: int = 1) -> None: """Show an info message in the statusbar. Args: text: The text to show. count: How many times to show the message """ for _ in range(count): message.info(text) @cmdutils.register() def message_warning(text: str) -> None: """Show a warning message in the statusbar. Args: text: The text to show. """ message.warning(text) @cmdutils.register(debug=True) @cmdutils.argument('typ', choices=['exception', 'segfault']) def debug_crash(typ: str = 'exception') -> None: """Crash for debugging purposes. Args: typ: either 'exception' or 'segfault'. """ if typ == 'segfault': os.kill(os.getpid(), signal.SIGSEGV) raise Exception("Segfault failed (wat.)") raise Exception("Forced crash") @cmdutils.register(debug=True, maxsplit=0, no_cmd_split=True) def debug_trace(expr: str = "") -> None: """Trace executed code via hunter. Args: expr: What to trace, passed to hunter. """ if hunter is None: raise cmdutils.CommandError("You need to install 'hunter' to use this " "command!") try: eval('hunter.trace({})'.format(expr)) except Exception as e: raise cmdutils.CommandError("{}: {}".format(e.__class__.__name__, e)) @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('position', completion=miscmodels.inspector_position) def devtools(tab: apitypes.Tab, position: apitypes.InspectorPosition = None) -> None: """Toggle the developer tools (web inspector). Args: position: Where to open the devtools (right/left/top/bottom/window). """ try: tab.private_api.toggle_inspector(position) except apitypes.InspectorError as e: raise cmdutils.CommandError(e) @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def devtools_focus(tab: apitypes.Tab) -> None: """Toggle focus between the devtools/tab.""" assert tab.data.splitter is not None try: tab.data.splitter.cycle_focus() except apitypes.InspectorError as e: raise cmdutils.CommandError(e)