1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 2 3# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org> 4# 5# This file is part of qutebrowser. 6# 7# qutebrowser is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# qutebrowser is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. 19 20"""Misc. utility commands exposed to the user.""" 21 22# QApplication and objects are imported so they're usable in :debug-pyeval 23 24import functools 25import os 26import traceback 27from typing import Optional 28 29from PyQt5.QtCore import QUrl 30from PyQt5.QtWidgets import QApplication 31 32from qutebrowser.browser import qutescheme 33from qutebrowser.utils import log, objreg, usertypes, message, debug, utils 34from qutebrowser.keyinput import modeman 35from qutebrowser.commands import runners 36from qutebrowser.api import cmdutils 37from qutebrowser.misc import ( # pylint: disable=unused-import 38 consolewidget, debugcachestats, objects, miscwidgets) 39from qutebrowser.utils.version import pastebin_version 40from qutebrowser.qt import sip 41 42 43@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True) 44@cmdutils.argument('win_id', value=cmdutils.Value.win_id) 45def later(duration: str, command: str, win_id: int) -> None: 46 """Execute a command after some time. 47 48 Args: 49 duration: Duration to wait in format XhYmZs or a number for milliseconds. 50 command: The command to run, with optional args. 51 """ 52 try: 53 ms = utils.parse_duration(duration) 54 except ValueError as e: 55 raise cmdutils.CommandError(e) 56 commandrunner = runners.CommandRunner(win_id) 57 timer = usertypes.Timer(name='later', parent=QApplication.instance()) 58 try: 59 timer.setSingleShot(True) 60 try: 61 timer.setInterval(ms) 62 except OverflowError: 63 raise cmdutils.CommandError("Numeric argument is too large for " 64 "internal int representation.") 65 timer.timeout.connect( 66 functools.partial(commandrunner.run_safely, command)) 67 timer.timeout.connect(timer.deleteLater) 68 timer.start() 69 except: 70 timer.deleteLater() 71 raise 72 73 74@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True) 75@cmdutils.argument('win_id', value=cmdutils.Value.win_id) 76@cmdutils.argument('count', value=cmdutils.Value.count) 77def repeat(times: int, command: str, win_id: int, count: int = None) -> None: 78 """Repeat a given command. 79 80 Args: 81 times: How many times to repeat. 82 command: The command to run, with optional args. 83 count: Multiplies with 'times' when given. 84 """ 85 if count is not None: 86 times *= count 87 88 if times < 0: 89 raise cmdutils.CommandError("A negative count doesn't make sense.") 90 commandrunner = runners.CommandRunner(win_id) 91 for _ in range(times): 92 commandrunner.run_safely(command) 93 94 95@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True) 96@cmdutils.argument('win_id', value=cmdutils.Value.win_id) 97@cmdutils.argument('count', value=cmdutils.Value.count) 98def run_with_count(count_arg: int, command: str, win_id: int, 99 count: int = 1) -> None: 100 """Run a command with the given count. 101 102 If run_with_count itself is run with a count, it multiplies count_arg. 103 104 Args: 105 count_arg: The count to pass to the command. 106 command: The command to run, with optional args. 107 count: The count that run_with_count itself received. 108 """ 109 runners.CommandRunner(win_id).run(command, count_arg * count) 110 111 112@cmdutils.register() 113def clear_messages() -> None: 114 """Clear all message notifications.""" 115 message.global_bridge.clear_messages.emit() 116 117 118@cmdutils.register(debug=True) 119def debug_all_objects() -> None: 120 """Print a list of all objects to the debug log.""" 121 s = debug.get_all_objects() 122 log.misc.debug(s) 123 124 125@cmdutils.register(debug=True) 126def debug_cache_stats() -> None: 127 """Print LRU cache stats.""" 128 debugcachestats.debug_cache_stats() 129 130 131@cmdutils.register(debug=True) 132def debug_console() -> None: 133 """Show the debugging console.""" 134 if consolewidget.console_widget is None: 135 log.misc.debug('initializing debug console') 136 consolewidget.init() 137 138 assert consolewidget.console_widget is not None 139 140 if consolewidget.console_widget.isVisible(): 141 log.misc.debug('hiding debug console') 142 consolewidget.console_widget.hide() 143 else: 144 log.misc.debug('showing debug console') 145 consolewidget.console_widget.show() 146 147 148@cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True) 149def debug_pyeval(s: str, file: bool = False, quiet: bool = False) -> None: 150 """Evaluate a python string and display the results as a web page. 151 152 Args: 153 s: The string to evaluate. 154 file: Interpret s as a path to file, also implies --quiet. 155 quiet: Don't show the output in a new tab. 156 """ 157 if file: 158 quiet = True 159 path = os.path.expanduser(s) 160 try: 161 with open(path, 'r', encoding='utf-8') as f: 162 s = f.read() 163 except OSError as e: 164 raise cmdutils.CommandError(str(e)) 165 try: 166 exec(s) 167 out = "No error" 168 except Exception: 169 out = traceback.format_exc() 170 else: 171 try: 172 r = eval(s) 173 out = repr(r) 174 except Exception: 175 out = traceback.format_exc() 176 177 qutescheme.pyeval_output = out 178 if quiet: 179 log.misc.debug("pyeval output: {}".format(out)) 180 else: 181 tabbed_browser = objreg.get('tabbed-browser', scope='window', 182 window='last-focused') 183 tabbed_browser.load_url(QUrl('qute://pyeval'), newtab=True) 184 185 186@cmdutils.register(debug=True) 187def debug_set_fake_clipboard(s: str = None) -> None: 188 """Put data into the fake clipboard and enable logging, used for tests. 189 190 Args: 191 s: The text to put into the fake clipboard, or unset to enable logging. 192 """ 193 if s is None: 194 utils.log_clipboard = True 195 else: 196 utils.fake_clipboard = s 197 198 199@cmdutils.register() 200@cmdutils.argument('win_id', value=cmdutils.Value.win_id) 201@cmdutils.argument('count', value=cmdutils.Value.count) 202def repeat_command(win_id: int, count: int = None) -> None: 203 """Repeat the last executed command. 204 205 Args: 206 count: Which count to pass the command. 207 """ 208 mode_manager = modeman.instance(win_id) 209 if mode_manager.mode not in runners.last_command: 210 raise cmdutils.CommandError("You didn't do anything yet.") 211 cmd = runners.last_command[mode_manager.mode] 212 commandrunner = runners.CommandRunner(win_id) 213 commandrunner.run(cmd[0], count if count is not None else cmd[1]) 214 215 216@cmdutils.register(debug=True, name='debug-log-capacity') 217def log_capacity(capacity: int) -> None: 218 """Change the number of log lines to be stored in RAM. 219 220 Args: 221 capacity: Number of lines for the log. 222 """ 223 if capacity < 0: 224 raise cmdutils.CommandError("Can't set a negative log capacity!") 225 assert log.ram_handler is not None 226 log.ram_handler.change_log_capacity(capacity) 227 228 229@cmdutils.register(debug=True) 230def debug_log_filter(filters: str) -> None: 231 """Change the log filter for console logging. 232 233 Args: 234 filters: A comma separated list of logger names. Can also be "none" to 235 clear any existing filters. 236 """ 237 if log.console_filter is None: 238 raise cmdutils.CommandError("No log.console_filter. Not attached " 239 "to a console?") 240 241 try: 242 new_filter = log.LogFilter.parse(filters) 243 except log.InvalidLogFilterError as e: 244 raise cmdutils.CommandError(e) 245 246 log.console_filter.update_from(new_filter) 247 248 249@cmdutils.register() 250@cmdutils.argument('current_win_id', value=cmdutils.Value.win_id) 251def window_only(current_win_id: int) -> None: 252 """Close all windows except for the current one.""" 253 for win_id, window in objreg.window_registry.items(): 254 255 # We could be in the middle of destroying a window here 256 if sip.isdeleted(window): 257 continue 258 259 if win_id != current_win_id: 260 window.close() 261 262 263@cmdutils.register() 264@cmdutils.argument('win_id', value=cmdutils.Value.win_id) 265def version(win_id: int, paste: bool = False) -> None: 266 """Show version information. 267 268 Args: 269 paste: Paste to pastebin. 270 """ 271 tabbed_browser = objreg.get('tabbed-browser', scope='window', 272 window=win_id) 273 tabbed_browser.load_url(QUrl('qute://version/'), newtab=True) 274 275 if paste: 276 pastebin_version() 277 278 279_keytester_widget: Optional[miscwidgets.KeyTesterWidget] = None 280 281 282@cmdutils.register(debug=True) 283def debug_keytester() -> None: 284 """Show a keytester widget.""" 285 global _keytester_widget 286 if (_keytester_widget and 287 not sip.isdeleted(_keytester_widget) and 288 _keytester_widget.isVisible()): 289 _keytester_widget.close() 290 else: 291 _keytester_widget = miscwidgets.KeyTesterWidget() 292 _keytester_widget.show() 293