def spawn(self, win_id, userscript=False, quiet=False, *args): """Spawn a command in a shell. Note the {url} variable which gets replaced by the current URL might be useful here. // We use subprocess rather than Qt's QProcess here because we really don't care about the process anymore as soon as it's spawned. Args: userscript: Run the command as an userscript. quiet: Don't print the commandline being executed. *args: The commandline to execute. """ log.procs.debug("Executing: {}, userscript={}".format( args, userscript)) if not quiet: fake_cmdline = ' '.join(shlex.quote(arg) for arg in args) message.info(win_id, 'Executing: ' + fake_cmdline) if userscript: cmd = args[0] args = [] if not args else args[1:] self.run_userscript(cmd, *args) else: try: subprocess.Popen(args) except OSError as e: raise cmdexc.CommandError("Error while spawning command: " "{}".format(e))
def on_lists_downloaded(self): """Install block lists after files have been downloaded.""" with open(self._local_hosts_file, 'w', encoding='utf-8') as f: for host in sorted(self._blocked_hosts): f.write(host + '\n') message.info("adblock: Read {} hosts from {} sources.".format( len(self._blocked_hosts), self._done_count))
def message_info(text): """Show an info message in the statusbar. Args: text: The text to show. """ message.info(text)
def yank(self, url, context): """Yank an element to the clipboard or primary selection. Args: url: The URL to open as a QUrl. context: The HintContext to use. """ sel = (context.target == Target.yank_primary and utils.supports_selection()) flags = QUrl.FullyEncoded | QUrl.RemovePassword if url.scheme() == 'mailto': flags |= QUrl.RemoveScheme urlstr = url.toString(flags) new_content = urlstr # only second and consecutive yanks are to append to the clipboard if context.rapid and not context.first_run: try: old_content = utils.get_clipboard(selection=sel) except utils.ClipboardEmptyError: pass else: new_content = os.linesep.join([old_content, new_content]) utils.set_clipboard(new_content, selection=sel) msg = "Yanked URL to {}: {}".format( "primary selection" if sel else "clipboard", urlstr) message.info(msg)
def on_finished(self, code, status): """Show a message when the process finished.""" self._started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) if status == QProcess.CrashExit: message.error(self._win_id, "{} crashed!".format(self._what.capitalize()), immediately=True) elif status == QProcess.NormalExit and code == 0: if self.verbose: message.info(self._win_id, "{} exited successfully.".format( self._what.capitalize())) else: assert status == QProcess.NormalExit # We call this 'status' here as it makes more sense to the user - # it's actually 'code'. message.error(self._win_id, "{} exited with status {}.".format( self._what.capitalize(), code)) stderr = bytes(self._proc.readAllStandardError()).decode('utf-8') stdout = bytes(self._proc.readAllStandardOutput()).decode('utf-8') if stdout: log.procs.error("Process stdout:\n" + stdout.strip()) if stderr: log.procs.error("Process stderr:\n" + stderr.strip())
def import_txt(self): """Import a history text file into sqlite if it exists. In older versions of qutebrowser, history was stored in a text format. This converts that file into the new sqlite format and moves it to a backup location. """ path = os.path.join(standarddir.data(), 'history') if not os.path.isfile(path): return def action(): with debug.log_time(log.init, 'Import old history file to sqlite'): try: self._read(path) except ValueError as ex: message.error('Failed to import history: {}'.format(ex)) else: bakpath = path + '.bak' message.info('History import complete. Moving {} to {}' .format(path, bakpath)) os.rename(path, bakpath) # delay to give message time to appear before locking down for import message.info('Converting {} to sqlite...'.format(path)) QTimer.singleShot(100, action)
def set_command(self, win_id: {'special': 'win_id'}, sectname: {'name': 'section'}, optname: {'name': 'option'}, value=None, temp=False): """Set an option. If the option name ends with '?', the value of the option is shown instead. // Wrapper for self.set() to output exceptions in the status bar. Args: sectname: The section where the option is in. optname: The name of the option. value: The value to set. temp: Set value temporarily. """ try: if optname.endswith('?'): val = self.get(sectname, optname[:-1], transformed=False) message.info(win_id, "{} {} = {}".format( sectname, optname[:-1], val), immediately=True) else: if value is None: raise cmdexc.CommandError("set: The following arguments " "are required: value") layer = 'temp' if temp else 'conf' self.set(layer, sectname, optname, value) except (NoOptionError, NoSectionError, configtypes.ValidationError, ValueError) as e: raise cmdexc.CommandError("set: {} - {}".format( e.__class__.__name__, e))
def _handle_wheel(self, e): """Zoom on Ctrl-Mousewheel. Args: e: The QWheelEvent. """ if self._ignore_wheel_event: # See https://github.com/qutebrowser/qutebrowser/issues/395 self._ignore_wheel_event = False return True if e.modifiers() & Qt.ControlModifier: divider = config.get('input', 'mouse-zoom-divider') if divider == 0: return False factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) if factor < 0: return False perc = int(100 * factor) message.info("Zoom level: {}%".format(perc), replace=True) self._tab.zoom.set_factor(factor) elif e.modifiers() & Qt.ShiftModifier: if e.angleDelta().y() > 0: self._tab.scroller.left() else: self._tab.scroller.right() return True return False
def _on_finished(self, code, status): """Show a message when the process finished.""" self._started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) encoding = locale.getpreferredencoding(do_setlocale=False) stderr = bytes(self._proc.readAllStandardError()).decode( encoding, 'replace') stdout = bytes(self._proc.readAllStandardOutput()).decode( encoding, 'replace') if status == QProcess.CrashExit: exitinfo = "{} crashed!".format(self._what.capitalize()) message.error(exitinfo) elif status == QProcess.NormalExit and code == 0: exitinfo = "{} exited successfully.".format( self._what.capitalize()) if self.verbose: message.info(exitinfo) else: assert status == QProcess.NormalExit # We call this 'status' here as it makes more sense to the user - # it's actually 'code'. exitinfo = ("{} exited with status {}, see :messages for " "details.").format(self._what.capitalize(), code) message.error(exitinfo) if stdout: log.procs.error("Process stdout:\n" + stdout.strip()) if stderr: log.procs.error("Process stderr:\n" + stderr.strip()) qutescheme.spawn_output = self._spawn_format(exitinfo, stdout, stderr)
def session_save(self, win_id, name: {'type': str}=default, current=False, quiet=False, force=False): """Save a session. Args: win_id: The current window ID. name: The name of the session. If not given, the session configured in general -> session-default-name is saved. current: Save the current session instead of the default. quiet: Don't show confirmation message. force: Force saving internal sessions (starting with an underline). """ if (name is not default and name.startswith('_') and # pylint: disable=no-member not force): raise cmdexc.CommandError("{} is an internal session, use --force " "to save anyways.".format(name)) if current: if self._current is None: raise cmdexc.CommandError("No session loaded currently!") name = self._current assert not name.startswith('_') try: name = self.save(name) except SessionError as e: raise cmdexc.CommandError("Error while saving session: {}" .format(e)) else: if not quiet: message.info(win_id, "Saved session {}.".format(name), immediately=True)
def _handle_wheel(self, e): """Zoom on Ctrl-Mousewheel. Args: e: The QWheelEvent. """ if self._ignore_wheel_event: # See https://github.com/qutebrowser/qutebrowser/issues/395 self._ignore_wheel_event = False return True if e.modifiers() & Qt.ControlModifier: mode = modeman.instance(self._tab.win_id).mode if mode == usertypes.KeyMode.passthrough: return False divider = config.val.zoom.mouse_divider if divider == 0: return False factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) if factor < 0: return False perc = int(100 * factor) message.info("Zoom level: {}%".format(perc), replace=True) self._tab.zoom.set_factor(factor) elif e.modifiers() & Qt.ShiftModifier: if e.angleDelta().y() > 0: self._tab.scroller.left() else: self._tab.scroller.right() return True return False
def session_save(self, name: str = default, current=False, quiet=False, force=False, only_active_window=False, with_private=False, win_id=None): """Save a session. Args: name: The name of the session. If not given, the session configured in general -> session-default-name is saved. current: Save the current session instead of the default. quiet: Don't show confirmation message. force: Force saving internal sessions (starting with an underline). only_active_window: Saves only tabs of the currently active window. with_private: Include private windows. """ if name is not default and name.startswith('_') and not force: raise cmdexc.CommandError("{} is an internal session, use --force " "to save anyways.".format(name)) if current: if self._current is None: raise cmdexc.CommandError("No session loaded currently!") name = self._current assert not name.startswith('_') try: if only_active_window: name = self.save(name, only_window=win_id, with_private=with_private) else: name = self.save(name, with_private=with_private) except SessionError as e: raise cmdexc.CommandError("Error while saving session: {}" .format(e)) else: if not quiet: message.info("Saved session {}.".format(name))
def set_command(self, win_id: {'special': 'win_id'}, sectname: {'name': 'section'}=None, optname: {'name': 'option'}=None, value=None, temp=False, print_val: {'name': 'print'}=False): """Set an option. If the option name ends with '?', the value of the option is shown instead. If the option name ends with '!' and it is a boolean value, toggle it. // Wrapper for self.set() to output exceptions in the status bar. Args: sectname: The section where the option is in. optname: The name of the option. value: The value to set. temp: Set value temporarily. print_val: Print the value after setting. """ if sectname is not None and optname is None: raise cmdexc.CommandError( "set: Either both section and option have to be given, or " "neither!") if sectname is None and optname is None: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) tabbed_browser.openurl(QUrl('qute:settings'), newtab=False) return if optname.endswith('?'): optname = optname[:-1] print_val = True else: try: if optname.endswith('!') and value is None: val = self.get(sectname, optname[:-1]) layer = 'temp' if temp else 'conf' if isinstance(val, bool): self.set(layer, sectname, optname[:-1], str(not val)) else: raise cmdexc.CommandError( "set: Attempted inversion of non-boolean value.") elif value is not None: layer = 'temp' if temp else 'conf' self.set(layer, sectname, optname, value) else: raise cmdexc.CommandError("set: The following arguments " "are required: value") except (configexc.Error, configparser.Error) as e: raise cmdexc.CommandError("set: {} - {}".format( e.__class__.__name__, e)) if print_val: val = self.get(sectname, optname, transformed=False) message.info(win_id, "{} {} = {}".format( sectname, optname, val), immediately=True)
def check_scroll_pos(): """Check if the scroll position got smaller and show info.""" if not backward and self.scroll_pos < old_scroll_pos: message.info(self.win_id, "Search hit BOTTOM, continuing " "at TOP", immediately=True) elif backward and self.scroll_pos > old_scroll_pos: message.info(self.win_id, "Search hit TOP, continuing at " "BOTTOM", immediately=True)
def _write_backup(self, path): bak = path + '.bak' message.info('History import complete. Appending {} to {}' .format(path, bak)) with open(path, 'r', encoding='utf-8') as infile: with open(bak, 'a', encoding='utf-8') as outfile: for line in infile: outfile.write('\n' + line) os.remove(path)
def _pre_start(self, cmd, args): """Prepare starting of a QProcess.""" if self._started: raise ValueError("Trying to start a running QProcess!") self.cmd = cmd self.args = args if self.verbose: fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args)) message.info(self._win_id, 'Executing: ' + fake_cmdline)
def message_info(text, count=1): """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)
def _print_value(self, option, pattern): """Print the value of the given option.""" with self._handle_config_error(): value = self._config.get_str(option, pattern=pattern) text = "{} = {}".format(option, value) if pattern is not None: text += " for {}".format(pattern) message.info(text)
def set_command(self, win_id, section_=None, option=None, value=None, temp=False, print_=False): """Set an option. If the option name ends with '?', the value of the option is shown instead. If the option name ends with '!' and it is a boolean value, toggle it. // Wrapper for self.set() to output exceptions in the status bar. Args: section_: The section where the option is in. option: The name of the option. value: The value to set. temp: Set value temporarily. print_: Print the value after setting. """ if section_ is not None and option is None: raise cmdexc.CommandError( "set: Either both section and option have to be given, or " "neither!") if section_ is None and option is None: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) tabbed_browser.openurl(QUrl('qute:settings'), newtab=False) return if option.endswith('?') and option != '?': option = option[:-1] print_ = True else: with self._handle_config_error(): if option.endswith('!') and option != '!' and value is None: option = option[:-1] val = self.get(section_, option) layer = 'temp' if temp else 'conf' if isinstance(val, bool): self.set(layer, section_, option, str(not val)) else: raise cmdexc.CommandError( "set: Attempted inversion of non-boolean value.") elif value is not None: layer = 'temp' if temp else 'conf' self.set(layer, section_, option, value) else: raise cmdexc.CommandError("set: The following arguments " "are required: value") if print_: with self._handle_config_error(): val = self.get(section_, option, transformed=False) message.info(win_id, "{} {} = {}".format( section_, option, val), immediately=True)
def backup(self): """Create a backup if the content has changed from the original.""" if not self._content: return try: fname = self._create_tempfile(self._content, 'qutebrowser-editor-backup-') message.info('Editor backup at {}'.format(fname)) except OSError as e: message.error('Failed to create editor backup: {}'.format(e))
def _pre_start(self, cmd, args): """Prepare starting of a QProcess.""" if self._started: raise ValueError("Trying to start a running QProcess!") self.cmd = cmd self.args = args fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args)) log.procs.debug("Executing: {}".format(fake_cmdline)) if self.verbose: message.info('Executing: ' + fake_cmdline)
def read_hosts(self): """Read hosts from the existing blocked-hosts file.""" self.blocked_hosts = set() if os.path.exists(self._hosts_file): with open(self._hosts_file, 'r', encoding='utf-8') as f: for line in f: self.blocked_hosts.add(line.strip()) else: if config.get('content', 'host-block-lists') is not None: message.info('last-focused', "Run :adblock-update to get adblock lists.")
def action(): with debug.log_time(log.init, 'Import old history file to sqlite'): try: self._read(path) except ValueError as ex: message.error('Failed to import history: {}'.format(ex)) else: bakpath = path + '.bak' message.info('History import complete. Moving {} to {}' .format(path, bakpath)) os.rename(path, bakpath)
def _on_mouse_wheel_zoom(self, delta): """Handle zooming via mousewheel requested by the web view.""" divider = config.get('input', 'mouse-zoom-divider') factor = self.factor() + delta.y() / divider if factor < 0: return perc = int(100 * factor) message.info(self._win_id, "Zoom level: {}%".format(perc)) self._neighborlist.fuzzyval = perc self._set_factor_internal(factor) self._default_zoom_changed = True
def _yank(self, url): """Yank an element to the clipboard or primary selection. Args: url: The URL to open as a QURL. """ sel = self._context.target == Target.yank_primary mode = QClipboard.Selection if sel else QClipboard.Clipboard urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) QApplication.clipboard().setText(urlstr, mode) message.info(self._win_id, "URL yanked to {}".format( "primary selection" if sel else "clipboard"))
def on_finished(self, code, status): """Show a message when the process finished.""" self._started = False log.procs.debug("Process finished with code {}, status {}.".format(code, status)) if status == QProcess.CrashExit: message.error(self._win_id, "{} crashed!".format(self._what.capitalize()), immediately=True) elif status == QProcess.NormalExit and code == 0: if self.verbose: message.info(self._win_id, "{} exited successfully.".format(self._what.capitalize())) else: assert status == QProcess.NormalExit message.error(self._win_id, "{} exited with status {}.".format(self._what.capitalize(), code))
def zoom_perc(self, perc, fuzzyval=True): """Zoom to a given zoom percentage. Args: perc: The zoom percentage as int. fuzzyval: Whether to set the NeighborLists fuzzyval. """ if fuzzyval: self._zoom.fuzzyval = int(perc) if perc < 0: raise cmdexc.CommandError("Can't zoom {}%!".format(perc)) self.setZoomFactor(float(perc) / 100) message.info(self._win_id, "Zoom level: {}%".format(perc))
def yank(self, url, context): """Yank an element to the clipboard or primary selection. Args: url: The URL to open as a QUrl. context: The HintContext to use. """ sel = context.target == Target.yank_primary and utils.supports_selection() urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) utils.set_clipboard(urlstr, selection=sel) msg = "Yanked URL to {}: {}".format("primary selection" if sel else "clipboard", urlstr) message.info(msg)
def read_hosts(self): """Read hosts from the existing blocked-hosts file.""" self.blocked_hosts = set() if os.path.exists(self._hosts_file): try: with open(self._hosts_file, 'r', encoding='utf-8') as f: for line in f: self.blocked_hosts.add(line.strip()) except OSError: log.misc.exception("Failed to read host blocklist!") else: if config.get('content', 'host-block-lists') is not None: message.info('current', "Run :adblock-update to get adblock lists.")
def read_hosts(self): """Read hosts from the existing blocked-hosts file.""" self._blocked_hosts = set() self._read_hosts_file(self._config_hosts_file, self._config_blocked_hosts) found = self._read_hosts_file(self._local_hosts_file, self._blocked_hosts) if not found: args = objreg.get('args') if (config.get('content', 'host-block-lists') is not None and args.basedir is None): message.info("Run :adblock-update to get adblock lists.")
def yank(self, url, context): """Yank an element to the clipboard or primary selection. Args: url: The URL to open as a QUrl. context: The HintContext to use. """ sel = (context.target == Target.yank_primary and utils.supports_selection()) flags = QUrl.FullyEncoded | QUrl.RemovePassword if url.scheme() == 'mailto': flags |= QUrl.RemoveScheme urlstr = url.toString(flags) utils.set_clipboard(urlstr, selection=sel) msg = "Yanked URL to {}: {}".format( "primary selection" if sel else "clipboard", urlstr) message.info(msg)
def read_hosts(self): """Read hosts from the existing blocked-hosts file.""" self._blocked_hosts = set() if self._local_hosts_file is None: return self._read_hosts_file(self._config_hosts_file, self._config_blocked_hosts) found = self._read_hosts_file(self._local_hosts_file, self._blocked_hosts) if not found: args = objreg.get('args') if (config.get('content', 'host-block-lists') is not None and args.basedir is None): message.info('current', "Run :adblock-update to get adblock lists.")
def bind(self, key, command=None, *, mode='normal', force=False): """Bind a key to a command. Args: key: The keychain or special key (inside `<...>`) to bind. command: The command to execute, with optional args, or None to print the current binding. mode: A comma-separated list of modes to bind the key in (default: `normal`). force: Rebind the key if it is already bound. """ if utils.is_special_key(key): # <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent key = key.lower() if command is None: cmd = self.get_bindings_for(mode).get(key, None) if cmd is None: message.info("{} is unbound in {} mode".format(key, mode)) else: message.info("{} is bound to '{}' in {} mode".format(key, cmd, mode)) return modenames = self._normalize_sectname(mode).split(',') for m in modenames: if m not in configdata.KEY_DATA: raise cmdexc.CommandError("Invalid mode {}!".format(m)) try: modes = [usertypes.KeyMode[m] for m in modenames] self._validate_command(command, modes) except KeyConfigError as e: raise cmdexc.CommandError(str(e)) try: self._add_binding(mode, key, command, force=force) except DuplicateKeychainError as e: raise cmdexc.CommandError("Duplicate keychain {} - use --force to " "override!".format(str(e.keychain))) except KeyConfigError as e: raise cmdexc.CommandError(e) for m in modenames: self.changed.emit(m) self._mark_config_dirty()
def prompt_yank(self, sel=False): """Yank URL to clipboard or primary selection. Args: sel: Use the primary selection instead of the clipboard. """ if self._prompt is None: raise AssertionError question = self._prompt.question if question.url is None: message.error('No URL found.') return if sel and utils.supports_selection(): target = 'primary selection' else: sel = False target = 'clipboard' utils.set_clipboard(question.url, sel) message.info("Yanked to {}: {}".format(target, question.url))
def debug_dump_history(self, dest): """Dump the history to a file in the old pre-SQL format. Args: dest: Where to write the file to. """ dest = os.path.expanduser(dest) lines = ('{}{} {} {}'.format(int(x.atime), '-r' * x.redirect, x.url, x.title) for x in self.select(sort_by='atime', sort_order='asc')) try: with open(dest, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) message.info("Dumped history to {}".format(dest)) except OSError as e: raise cmdutils.CommandError( 'Could not write history: {}'.format(e))
def bind(self, win_id: str, key: str = None, command: str = None, *, mode: str = 'normal', default: bool = False) -> None: """Bind a key to a command. If no command is given, show the current binding for the given key. Using :bind without any arguments opens a page showing all keybindings. Args: key: The keychain to bind. Examples of valid keychains are `gC`, `<Ctrl-X>` or `<Ctrl-C>a`. command: The command to execute, with optional args. mode: The mode to bind the key in (default: `normal`). See `:help bindings.commands` for the available modes. default: If given, restore a default binding. """ if key is None: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) tabbed_browser.load_url(QUrl('qute://bindings'), newtab=True) return seq = self._parse_key(key) if command is None: if default: # :bind --default: Restore default with self._handle_config_error(): self._keyconfig.bind_default(seq, mode=mode, save_yaml=True) return # No --default -> print binding with self._handle_config_error(): cmd = self._keyconfig.get_command(seq, mode) if cmd is None: message.info("{} is unbound in {} mode".format(seq, mode)) else: message.info("{} is bound to '{}' in {} mode".format( seq, cmd, mode)) return with self._handle_config_error(): self._keyconfig.bind(seq, command, mode=mode, save_yaml=True)
def wheelEvent(self, e): """Zoom on Ctrl-Mousewheel. Args: e: The QWheelEvent. """ if e.modifiers() & Qt.ControlModifier: e.accept() divider = config.get('input', 'mouse-zoom-divider') factor = self.zoomFactor() + e.angleDelta().y() / divider if factor < 0: return perc = int(100 * factor) message.info(self.win_id, "Zoom level: {}%".format(perc)) self._zoom.fuzzyval = perc self.setZoomFactor(factor) self._default_zoom_changed = True else: super().wheelEvent(e)
def session_save(self, name: typing.Union[str, Sentinel] = default, current: bool = False, quiet: bool = False, force: bool = False, only_active_window: bool = False, with_private: bool = False, win_id: int = None) -> None: """Save a session. Args: name: The name of the session. If not given, the session configured in session.default_name is saved. current: Save the current session instead of the default. quiet: Don't show confirmation message. force: Force saving internal sessions (starting with an underline). only_active_window: Saves only tabs of the currently active window. with_private: Include private windows. """ if (not isinstance(name, Sentinel) and name.startswith('_') and not force): raise cmdutils.CommandError("{} is an internal session, use " "--force to save anyways." .format(name)) if current: if self._current is None: raise cmdutils.CommandError("No session loaded currently!") name = self._current assert not name.startswith('_') try: if only_active_window: name = self.save(name, only_window=win_id, with_private=True) else: name = self.save(name, with_private=with_private) except SessionError as e: raise cmdutils.CommandError("Error while saving session: {}" .format(e)) else: if quiet: log.sessions.debug("Saved session {}.".format(name)) else: message.info("Saved session {}.".format(name))
def _on_finished(self, code, status): """Show a message when the process finished.""" self._started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) encoding = locale.getpreferredencoding(do_setlocale=False) stderr = self._proc.readAllStandardError().data().decode( encoding, 'replace') stdout = self._proc.readAllStandardOutput().data().decode( encoding, 'replace') if self._output_messages: if stdout: message.info(stdout.strip()) if stderr: message.error(stderr.strip()) if status == QProcess.CrashExit: exitinfo = "{} crashed.".format(self._what.capitalize()) message.error(exitinfo) elif status == QProcess.NormalExit and code == 0: exitinfo = "{} exited successfully.".format( self._what.capitalize()) if self.verbose: message.info(exitinfo) else: assert status == QProcess.NormalExit # We call this 'status' here as it makes more sense to the user - # it's actually 'code'. exitinfo = ("{} exited with status {}, see :messages for " "details.").format(self._what.capitalize(), code) message.error(exitinfo) if stdout: log.procs.error("Process stdout:\n" + stdout.strip()) if stderr: log.procs.error("Process stderr:\n" + stderr.strip()) qutescheme.spawn_output = self._spawn_format(exitinfo, stdout, stderr) self.final_stdout = stdout self.final_stderr = stderr
def prompt_fileselect_external(self): """Choose a location using a configured external picker. This spawns the external fileselector configured via `fileselect.folder.command`. """ assert self._prompt is not None if not isinstance(self._prompt, FilenamePrompt): raise cmdutils.CommandError( "Can only launch external fileselect for FilenamePrompt, " f"not {self._prompt.__class__.__name__}") # XXX to avoid current cyclic import from qutebrowser.browser import shared folders = shared.choose_file(shared.FileSelectionMode.folder) if not folders: message.info("No folder chosen.") return # choose_file already checks that this is max one folder assert len(folders) == 1 self.prompt_accept(folders[0])
def on_finished(self, code, status): """Show a message when the process finished.""" self._started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) if status == QProcess.CrashExit: message.error(self._win_id, "{} crashed!".format(self._what.capitalize()), immediately=True) elif status == QProcess.NormalExit and code == 0: if self.verbose: message.info( self._win_id, "{} exited successfully.".format(self._what.capitalize())) else: assert status == QProcess.NormalExit message.error( self._win_id, "{} exited with status {}.".format(self._what.capitalize(), code))
def set_command(self, win_id: {'special': 'win_id'}, sectname: {'name': 'section'}=None, optname: {'name': 'option'}=None, value=None, temp=False): """Set an option. If the option name ends with '?', the value of the option is shown instead. // Wrapper for self.set() to output exceptions in the status bar. Args: sectname: The section where the option is in. optname: The name of the option. value: The value to set. temp: Set value temporarily. """ if sectname is not None and optname is None: raise cmdexc.CommandError( "set: Either both section and option have to be given, or " "neither!") if sectname is None and optname is None: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) tabbed_browser.openurl(QUrl('qute:settings'), newtab=False) return try: if optname.endswith('?'): val = self.get(sectname, optname[:-1], transformed=False) message.info(win_id, "{} {} = {}".format( sectname, optname[:-1], val), immediately=True) else: if value is None: raise cmdexc.CommandError("set: The following arguments " "are required: value") layer = 'temp' if temp else 'conf' self.set(layer, sectname, optname, value) except (configexc.Error, configparser.Error) as e: raise cmdexc.CommandError("set: {} - {}".format( e.__class__.__name__, e))
def _selection_callback(text): if not text and not quiet: message.info("Nothing selected") def inthread(): with tempfile.NamedTemporaryFile(mode='wt', encoding='utf-8') as infile: infile.write(text) infile.flush() tempname = '"' + infile.name + '"' cmd = f"x-terminal-emulator-exe {prefix} 'cat {tempname} | {program} {maybeshell}'" p = subprocess.Popen(['sh', '-c', cmd]) p.wait() if p.returncode != 0 and not quiet: message.error( f'shell-send process "{cmd}" failed with: {p.returncode}' ) threading.Thread(target=inthread).start()
def _handle_wheel(self, e): """Zoom on Ctrl-Mousewheel. Args: e: The QWheelEvent. """ if self._ignore_wheel_event: # See https://github.com/The-Compiler/qutebrowser/issues/395 self._ignore_wheel_event = False return True if e.modifiers() & Qt.ControlModifier: divider = config.get('input', 'mouse-zoom-divider') factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) if factor < 0: return False perc = int(100 * factor) message.info(self._tab.win_id, "Zoom level: {}%".format(perc)) self._tab.zoom.set_factor(factor) return False
def session_save(self, name: str = default, current=False, quiet=False, force=False, only_active_window=False, with_private=False, win_id=None): """Save a session. Args: name: The name of the session. If not given, the session configured in general -> session-default-name is saved. current: Save the current session instead of the default. quiet: Don't show confirmation message. force: Force saving internal sessions (starting with an underline). only_active_window: Saves only tabs of the currently active window. with_private: Include private windows. """ if name is not default and name.startswith('_') and not force: raise cmdexc.CommandError("{} is an internal session, use --force " "to save anyways.".format(name)) if current: if self._current is None: raise cmdexc.CommandError("No session loaded currently!") name = self._current assert not name.startswith('_') try: if only_active_window: name = self.save(name, only_window=win_id, with_private=with_private) else: name = self.save(name, with_private=with_private) except SessionError as e: raise cmdexc.CommandError( "Error while saving session: {}".format(e)) else: if not quiet: message.info("Saved session {}.".format(name))
def _handle_wheel(self, e): """Zoom on Ctrl-Mousewheel. Args: e: The QWheelEvent. Return: True if the event should be filtered, False otherwise. """ if self._ignore_wheel_event: # See https://github.com/qutebrowser/qutebrowser/issues/395 self._ignore_wheel_event = False return True elif e.modifiers() & Qt.ControlModifier: mode = modeman.instance(self._tab.win_id).mode if mode == usertypes.KeyMode.passthrough: return False divider = config.val.zoom.mouse_divider if divider == 0: # Disable mouse zooming return True factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) if factor < 0: return True perc = int(100 * factor) message.info("Zoom level: {}%".format(perc), replace=True) self._tab.zoom.set_factor(factor) return True elif (e.modifiers() & Qt.ShiftModifier and not qtutils.version_check('5.9', compiled=False)): if e.angleDelta().y() > 0: self._tab.scroller.left() else: self._tab.scroller.right() return True return False
def _pre_start(self, cmd: str, args: Sequence[str]) -> None: """Resolve the given command and prepare starting of a QProcess. Doing the resolving in Python here instead of letting Qt do it serves two purposes: - Being able to show a nicer error message without having to parse the string we get from Qt: https://bugreports.qt.io/browse/QTBUG-44769 - Not running the file from the current directory on Unix with Qt < 5.15.? and 6.2.4, as a WORKAROUND for CVE-2022-25255: https://invent.kde.org/qt/qt/qtbase/-/merge_requests/139 https://www.qt.io/blog/security-advisory-qprocess https://lists.qt-project.org/pipermail/announce/2022-February/000333.html """ if self.outcome.running: raise ValueError("Trying to start a running QProcess!") self.cmd = cmd self.resolved_cmd = shutil.which(cmd) self.args = args log.procs.debug(f"Executing: {self}") if self.verbose: message.info(f'Executing: {self}')
def _handle_wheel(self, e): """Zoom on Ctrl-Mousewheel. Args: e: The QWheelEvent. Return: True if the event should be filtered, False otherwise. """ if self._ignore_wheel_event: # See https://github.com/qutebrowser/qutebrowser/issues/395 self._ignore_wheel_event = False return True # Don't allow scrolling while hinting mode = modeman.instance(self._tab.win_id).mode if mode == usertypes.KeyMode.hint: return True elif e.modifiers() & Qt.ControlModifier: if mode == usertypes.KeyMode.passthrough: return False divider = config.val.zoom.mouse_divider if divider == 0: # Disable mouse zooming return True factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) if factor < 0: return True perc = int(100 * factor) message.info(f"Zoom level: {perc}%", replace='zoom-level') self._tab.zoom.set_factor(factor) return True return False
def wheelEvent(self, e): """Zoom on Ctrl-Mousewheel. Args: e: The QWheelEvent. """ if self._ignore_wheel_event: self._ignore_wheel_event = False # See https://github.com/The-Compiler/qutebrowser/issues/395 return if e.modifiers() & Qt.ControlModifier: e.accept() divider = config.get('input', 'mouse-zoom-divider') factor = self.zoomFactor() + e.angleDelta().y() / divider if factor < 0: return perc = int(100 * factor) message.info(self.win_id, "Zoom level: {}%".format(perc)) self._zoom.fuzzyval = perc self.setZoomFactor(factor) self._default_zoom_changed = True else: super().wheelEvent(e)
def import_txt(self): """Import a history text file into sqlite if it exists. In older versions of qutebrowser, history was stored in a text format. This converts that file into the new sqlite format and moves it to a backup location. """ path = os.path.join(standarddir.data(), 'history') if not os.path.isfile(path): return def action(): with debug.log_time(log.init, 'Import old history file to sqlite'): try: self._read(path) except ValueError as ex: message.error('Failed to import history: {}'.format(ex)) else: self._write_backup(path) # delay to give message time to appear before locking down for import message.info('Converting {} to sqlite...'.format(path)) QTimer.singleShot(100, action)
def yank(self, title=False, sel=False): """Yank the current URL/title to the clipboard or primary selection. Args: sel: Use the primary selection instead of the clipboard. title: Yank the title instead of the URL. """ clipboard = QApplication.clipboard() if title: s = self._tabbed_browser().tabText(self._current_index()) else: s = self._current_url().toString(QUrl.FullyEncoded | QUrl.RemovePassword) if sel and clipboard.supportsSelection(): mode = QClipboard.Selection target = "primary selection" else: mode = QClipboard.Clipboard target = "clipboard" log.misc.debug("Yanking to {}: '{}'".format(target, s)) clipboard.setText(s, mode) what = 'Title' if title else 'URL' message.info(self._win_id, "{} yanked to {}".format(what, target))
def bind(self, key, command=None, *, mode='normal', default=False): """Bind a key to a command. Args: key: The keychain or special key (inside `<...>`) to bind. command: The command to execute, with optional args, or None to print the current binding. mode: A comma-separated list of modes to bind the key in (default: `normal`). See `:help bindings.commands` for the available modes. default: If given, restore a default binding. """ if command is None: if default: # :bind --default: Restore default with self._handle_config_error(): self._keyconfig.bind_default(key, mode=mode, save_yaml=True) return # No --default -> print binding if utils.is_special_key(key): # self._keyconfig.get_command does this, but we also need it # normalized for the output below key = utils.normalize_keystr(key) with self._handle_config_error(): cmd = self._keyconfig.get_command(key, mode) if cmd is None: message.info("{} is unbound in {} mode".format(key, mode)) else: message.info("{} is bound to '{}' in {} mode".format( key, cmd, mode)) return with self._handle_config_error(): self._keyconfig.bind(key, command, mode=mode, save_yaml=True)
def set_command(self, win_id: {'special': 'win_id'}, sectname: {'name': 'section'}, optname: {'name': 'option'}, value=None, temp=False): """Set an option. If the option name ends with '?', the value of the option is shown instead. // Wrapper for self.set() to output exceptions in the status bar. Args: sectname: The section where the option is in. optname: The name of the option. value: The value to set. temp: Set value temporarily. """ try: if optname.endswith('?'): val = self.get(sectname, optname[:-1], transformed=False) message.info(win_id, "{} {} = {}".format(sectname, optname[:-1], val), immediately=True) else: if value is None: raise cmdexc.CommandError("set: The following arguments " "are required: value") layer = 'temp' if temp else 'conf' self.set(layer, sectname, optname, value) except (NoOptionError, NoSectionError, configtypes.ValidationError, ValueError) as e: raise cmdexc.CommandError("set: {} - {}".format( e.__class__.__name__, e))
def bind(self, key, command=None, *, mode='normal', force=False): """Bind a key to a command. Args: key: The keychain or special key (inside `<...>`) to bind. command: The command to execute, with optional args, or None to print the current binding. mode: A comma-separated list of modes to bind the key in (default: `normal`). See `:help bindings.commands` for the available modes. force: Rebind the key if it is already bound. """ if command is None: if utils.is_special_key(key): # self._keyconfig.get_command does this, but we also need it # normalized for the output below key = utils.normalize_keystr(key) cmd = self._keyconfig.get_command(key, mode) if cmd is None: message.info("{} is unbound in {} mode".format(key, mode)) else: message.info("{} is bound to '{}' in {} mode".format( key, cmd, mode)) return try: self._keyconfig.bind(key, command, mode=mode, force=force, save_yaml=True) except configexc.DuplicateKeyError as e: raise cmdexc.CommandError( "bind: {} - use --force to override!".format(e)) except configexc.KeybindingError as e: raise cmdexc.CommandError("bind: {}".format(e))
def on_finished(self, code, status): """Show a message when the process finished.""" self._started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) if status == QProcess.CrashExit: message.error("{} crashed!".format(self._what.capitalize())) elif status == QProcess.NormalExit and code == 0: if self.verbose: message.info("{} exited successfully.".format( self._what.capitalize())) else: assert status == QProcess.NormalExit # We call this 'status' here as it makes more sense to the user - # it's actually 'code'. message.error("{} exited with status {}.".format( self._what.capitalize(), code)) stderr = bytes(self._proc.readAllStandardError()).decode('utf-8') stdout = bytes(self._proc.readAllStandardOutput()).decode('utf-8') if stdout: log.procs.error("Process stdout:\n" + stdout.strip()) if stderr: log.procs.error("Process stderr:\n" + stderr.strip())
def _yank_url(url: str) -> None: utils.set_clipboard(url) message.info("Version url {} yanked to clipboard.".format(url))
def _print_value(self, option): """Print the value of the given option.""" with self._handle_config_error(): value = self._config.get_str(option) message.info("{} = {}".format(option, value))
def _on_ready_read_stdout(self) -> None: if not self._output_messages: return self._process_text(self._proc.readAllStandardOutput(), 'stdout') message.info(self._elide_output(self.stdout), replace=f"stdout-{self.pid}")
def _open_special_pages(args): """Open special notification pages which are only shown once. Args: args: The argparse namespace. """ if args.basedir is not None: # With --basedir given, don't open anything. return general_sect = configfiles.state['general'] tabbed_browser = objreg.get('tabbed-browser', scope='window', window='last-focused') pages = [ # state, condition, URL ('quickstart-done', True, 'https://www.qutebrowser.org/quickstart.html'), ('config-migration-shown', os.path.exists(os.path.join(standarddir.config(), 'qutebrowser.conf')), 'qute://help/configuring.html'), ('webkit-warning-shown', objects.backend == usertypes.Backend.QtWebKit, 'qute://warning/webkit'), ('session-warning-shown', qtutils.version_check('5.15', compiled=False), 'qute://warning/sessions'), ] if 'quickstart-done' not in general_sect: # New users aren't going to be affected by the Qt 5.15 session change much, as # they aren't used to qutebrowser saving the full back/forward history in # sessions. general_sect['session-warning-shown'] = '1' for state, condition, url in pages: if general_sect.get(state) != '1' and condition: tabbed_browser.tabopen(QUrl(url), background=False) general_sect[state] = '1' # Show changelog on new releases change = configfiles.state.qutebrowser_version_changed if change == configfiles.VersionChange.equal: return setting = config.val.changelog_after_upgrade if not change.matches_filter(setting): log.init.debug( f"Showing changelog is disabled (setting {setting}, change {change})") return try: changelog = resources.read_file('html/doc/changelog.html') except OSError as e: log.init.warning(f"Not showing changelog due to {e}") return qbversion = qutebrowser.__version__ if f'id="v{qbversion}"' not in changelog: log.init.warning("Not showing changelog (anchor not found)") return message.info(f"Showing changelog after upgrade to qutebrowser v{qbversion}.") changelog_url = f'qute://help/changelog.html#v{qbversion}' tabbed_browser.tabopen(QUrl(changelog_url), background=False)
def set_command(self, win_id, section_=None, option=None, value=None, temp=False, print_=False): """Set an option. If the option name ends with '?', the value of the option is shown instead. If the option name ends with '!' and it is a boolean value, toggle it. // Wrapper for self.set() to output exceptions in the status bar. Args: section_: The section where the option is in. option: The name of the option. value: The value to set. temp: Set value temporarily. print_: Print the value after setting. """ if section_ is not None and option is None: raise cmdexc.CommandError( "set: Either both section and option have to be given, or " "neither!") if section_ is None and option is None: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) tabbed_browser.openurl(QUrl('qute:settings'), newtab=False) return if option.endswith('?') and option != '?': option = option[:-1] print_ = True else: with self._handle_config_error(): if option.endswith('!') and option != '!' and value is None: option = option[:-1] val = self.get(section_, option) layer = 'temp' if temp else 'conf' if isinstance(val, bool): self.set(layer, section_, option, str(not val)) else: raise cmdexc.CommandError( "set: Attempted inversion of non-boolean value.") elif value is not None: layer = 'temp' if temp else 'conf' self.set(layer, section_, option, value) else: raise cmdexc.CommandError("set: The following arguments " "are required: value") if print_: with self._handle_config_error(): val = self.get(section_, option, transformed=False) message.info(win_id, "{} {} = {}".format(section_, option, val), immediately=True)