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