diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f9eeacfcb..490907589 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -34,7 +34,7 @@ import qutebrowser.browser.hints as hints import qutebrowser.utils.url as urlutils import qutebrowser.utils.message as message import qutebrowser.utils.webelem as webelem -from qutebrowser.utils.misc import check_overflow +from qutebrowser.utils.misc import check_overflow, shell_escape, ExternalEditor from qutebrowser.utils.misc import shell_escape from qutebrowser.commands.exceptions import CommandError @@ -51,6 +51,7 @@ class CommandDispatcher(QObject): Attributes: _tabs: The TabbedBrowser object. + _editor: The ExternalEditor object. """ def __init__(self, parent): @@ -61,6 +62,7 @@ class CommandDispatcher(QObject): """ super().__init__(parent) self._tabs = parent + self._editor = None def _scroll_percent(self, perc=None, count=None, orientation=None): """Inner logic for scroll_percent_(x|y). @@ -601,56 +603,24 @@ class CommandDispatcher(QObject): elem = frame.findFirstElement(webelem.SELECTORS[ webelem.Group.editable_focused]) if elem.isNull(): - raise CommandError("No editable element focused!") - oshandle, filename = mkstemp(text=True) + message.error("No editable element focused!") + return text = elem.evaluateJavaScript('this.value') - if text: - with open(filename, 'w', encoding='utf-8') as f: - f.write(text) - proc = QProcess(self) - proc.finished.connect(partial(self.on_editor_closed, elem, oshandle, - filename)) - proc.error.connect(partial(self.on_editor_error, oshandle, filename)) - editor = config.get('general', 'editor') - executable = editor[0] - args = [arg.replace('{}', filename) for arg in editor[1:]] - logging.debug("Calling '{}' with args {}".format(executable, args)) - proc.start(executable, args) + self._editor = ExternalEditor() + self._editor.editing_finished.connect(partial(self.on_editing_finished, elem)) + self._editor.edit(text) - def on_editor_closed(self, elem, oshandle, filename, exitcode, - exitstatus): + def on_editing_finished(self, elem, text): """Write the editor text into the form field and clean up tempfile. Callback for QProcess when the editor was closed. - """ - logging.debug("Editor closed") - if exitcode != 0: - raise CommandError("Editor did quit abnormally (status " - "{})!".format(exitcode)) - if exitstatus != QProcess.NormalExit: - # No error here, since we already handle this in on_editor_error - return - if elem.isNull(): - raise CommandError("Element vanished while editing!") - with open(filename, 'r', encoding='utf-8') as f: - text = ''.join(f.readlines()) - text = webelem.javascript_escape(text) - logging.debug("Read back: {}".format(text)) - elem.evaluateJavaScript("this.value='{}'".format(text)) - self._editor_cleanup(oshandle, filename) - def on_editor_error(self, oshandle, filename, error): - """Display an error message and clean up when editor crashed.""" - messages = { - QProcess.FailedToStart: "The process failed to start.", - QProcess.Crashed: "The process crashed.", - QProcess.Timedout: "The last waitFor...() function timed out.", - QProcess.WriteError: ("An error occurred when attempting to write " - "to the process."), - QProcess.ReadError: ("An error occurred when attempting to read " - "from the process."), - QProcess.UnknownError: "An unknown error occurred.", - } - self._editor_cleanup(oshandle, filename) - raise CommandError("Error while calling editor: {}".format( - messages[error])) + Args: + elem: The QWebElement which was modified. + text: The new text to insert. + """ + if elem.isNull(): + message.error("Element vanished while editing!") + return + text = webelem.javascript_escape(text) + elem.evaluateJavaScript("this.value='{}'".format(text)) diff --git a/qutebrowser/utils/misc.py b/qutebrowser/utils/misc.py index 8367864b1..0ceb22850 100644 --- a/qutebrowser/utils/misc.py +++ b/qutebrowser/utils/misc.py @@ -27,13 +27,19 @@ import re import sys import shlex import os.path +import logging import urllib.request +from tempfile import mkstemp from urllib.parse import urljoin, urlencode from functools import reduce -from PyQt5.QtCore import QCoreApplication, QStandardPaths +from PyQt5.QtCore import (pyqtSignal, QCoreApplication, QStandardPaths, + QProcess, QObject) +from pkg_resources import resource_string import qutebrowser +import qutebrowser.config.config as config +import qutebrowser.utils.message as message MAXVALS = { @@ -239,3 +245,90 @@ def actute_warning(): "https://bugs.freedesktop.org/show_bug.cgi?id=69476 for " "details.") break + + +class ExternalEditor(QObject): + + """Class to simplify editing a text in an external editor.""" + + editing_finished = pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self.text = None + self.oshandle = None + self.filename = None + self.proc = None + + def _cleanup(self): + os.close(self.oshandle) + try: + os.remove(self.filename) + except PermissionError: + message.error("Failed to delete tempfile...") + self.text = None + self.oshandle = None + self.filename = None + self.proc = None + + def on_proc_closed(self, exitcode, exitstatus): + """Write the editor text into the form field and clean up tempfile. + + Callback for QProcess when the editor was closed. + + Emit: + editing_finished: If process exited normally. + """ + logging.debug("Editor closed") + if exitcode != 0: + message.error("Editor did quit abnormally (status {})!".format( + exitcode)) + return + if exitstatus != QProcess.NormalExit: + # No error here, since we already handle this in on_editor_error + return + with open(self.filename, 'r') as f: + text = ''.join(f.readlines()) + logging.debug("Read back: {}".format(text)) + self._cleanup() + self.editing_finished.emit(text) + + def on_proc_error(self, error): + """Display an error message and clean up when editor crashed.""" + messages = { + QProcess.FailedToStart: "The process failed to start.", + QProcess.Crashed: "The process crashed.", + QProcess.Timedout: "The last waitFor...() function timed out.", + QProcess.WriteError: ("An error occurred when attempting to write " + "to the process."), + QProcess.ReadError: ("An error occurred when attempting to read " + "from the process."), + QProcess.UnknownError: "An unknown error occurred.", + } + message.error("Error while calling editor: {}".format(messages[error])) + self._cleanup() + + def edit(self, text): + """Edit a given text. + + Args: + text: The initial text to edit. + + Emit: + editing_finished with the new text if editing finshed successfully. + """ + if self.text is not None: + raise ValueError("Already editing a file!") + self.text = text + self.oshandle, self.filename = mkstemp(text=True) + if not text: + with open(self.filename, 'w') as f: + f.write(text) + self.proc = QProcess(self) + self.proc.finished.connect(self.on_proc_closed) + self.proc.error.connect(self.on_proc_error) + editor = config.get('general', 'editor') + executable = editor[0] + args = [arg.replace('{}', self.filename) for arg in editor[1:]] + logging.debug("Calling \"{}\" with args {}".format(executable, args)) + self.proc.start(executable, args)