diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index bd84417d7..5fb2c9e6e 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1420,6 +1420,7 @@ How many steps to zoom out. |<>|Accept the current prompt. |<>|Shift the focus of the prompt file completion menu to another item. |<>|Immediately open a download. +|<>|Yank URL to clipboard or primary selection. |<>|Move back a character. |<>|Delete the character before the cursor. |<>|Remove chars from the cursor to the beginning of the word. @@ -1625,6 +1626,15 @@ If no specific command is given, this will use the system's default application ==== note * This command does not split arguments after the last argument and handles quotes literally. +[[prompt-yank]] +=== prompt-yank +Syntax: +:prompt-yank [*--sel*]+ + +Yank URL to clipboard or primary selection. + +==== optional arguments +* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. + [[rl-backward-char]] === rl-backward-char Move back a character. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 241cdf1aa..3826e6e84 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -617,6 +617,8 @@ Default: * +pass:[<Alt-Backspace>]+: +pass:[rl-backward-kill-word]+ * +pass:[<Alt-D>]+: +pass:[rl-kill-word]+ * +pass:[<Alt-F>]+: +pass:[rl-forward-word]+ +* +pass:[<Alt-Shift-Y>]+: +pass:[prompt-yank --sel]+ +* +pass:[<Alt-Y>]+: +pass:[prompt-yank]+ * +pass:[<Ctrl-?>]+: +pass:[rl-delete-char]+ * +pass:[<Ctrl-A>]+: +pass:[rl-beginning-of-line]+ * +pass:[<Ctrl-B>]+: +pass:[rl-backward-char]+ diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 12c96db7e..4f390b18b 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -31,7 +31,7 @@ import enum import sip from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex, - QTimer, QAbstractListModel) + QTimer, QAbstractListModel, QUrl) from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.config import config @@ -166,6 +166,7 @@ def get_filename_question(*, suggested_filename, url, parent=None): q.title = "Save file to:" q.text = "Please enter a location for {}".format( html.escape(url.toDisplayString())) + q.url = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) q.mode = usertypes.PromptMode.download q.completed.connect(q.deleteLater) q.default = _path_suggestion(suggested_filename) diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 7a8c5aa64..746fd8522 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -20,6 +20,7 @@ """Download manager.""" import io +import os.path import shutil import functools @@ -198,21 +199,23 @@ class DownloadItem(downloads.AbstractDownloadItem): def _ask_confirm_question(self, title, msg): no_action = functools.partial(self.cancel, remove_data=False) + url = 'file://{}'.format(self._filename) message.confirm_async(title=title, text=msg, yes_action=self._after_set_filename, no_action=no_action, cancel_action=no_action, - abort_on=[self.cancelled, self.error]) + abort_on=[self.cancelled, self.error], url=url) def _ask_create_parent_question(self, title, msg, force_overwrite, remember_directory): no_action = functools.partial(self.cancel, remove_data=False) + url = 'file://{}'.format(os.path.dirname(self._filename)) message.confirm_async(title=title, text=msg, yes_action=(lambda: self._after_create_parent_question( force_overwrite, remember_directory)), no_action=no_action, cancel_action=no_action, - abort_on=[self.cancelled, self.error]) + abort_on=[self.cancelled, self.error], url=url) def _set_fileobj(self, fileobj, *, autoclose=True): """Set the file object to write the download to. diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 5be2acd55..d82b741e5 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -23,13 +23,14 @@ import os import html import netrc +from PyQt5.QtCore import QUrl + from qutebrowser.config import config from qutebrowser.utils import usertypes, message, log, objreg, jinja, utils from qutebrowser.mainwindow import mainwindow class CallSuper(Exception): - """Raised when the caller should call the superclass instead.""" @@ -63,9 +64,10 @@ def authentication_required(url, authenticator, abort_on): else: msg = '{} needs authentication'.format( html.escape(url.toDisplayString())) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) answer = message.ask(title="Authentication required", text=msg, mode=usertypes.PromptMode.user_pwd, - abort_on=abort_on) + abort_on=abort_on, url=urlstr) if answer is not None: authenticator.setUser(answer.user) authenticator.setPassword(answer.password) @@ -80,9 +82,10 @@ def javascript_confirm(url, js_msg, abort_on): msg = 'From {}:
{}'.format(html.escape(url.toDisplayString()), html.escape(js_msg)) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) ans = message.ask('Javascript confirm', msg, mode=usertypes.PromptMode.yesno, - abort_on=abort_on) + abort_on=abort_on, url=urlstr) return bool(ans) @@ -96,10 +99,11 @@ def javascript_prompt(url, js_msg, default, abort_on): msg = '{} asks:
{}'.format(html.escape(url.toDisplayString()), html.escape(js_msg)) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) answer = message.ask('Javascript prompt', msg, mode=usertypes.PromptMode.text, default=default, - abort_on=abort_on) + abort_on=abort_on, url=urlstr) if answer is None: return (False, "") @@ -118,8 +122,9 @@ def javascript_alert(url, js_msg, abort_on): msg = 'From {}:
{}'.format(html.escape(url.toDisplayString()), html.escape(js_msg)) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert, - abort_on=abort_on) + abort_on=abort_on, url=urlstr) def javascript_log_message(level, source, line, msg): @@ -166,9 +171,10 @@ def ignore_certificate_errors(url, errors, abort_on): """.strip()) msg = err_template.render(url=url, errors=errors) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) ignore = message.ask(title="Certificate errors - continue?", text=msg, mode=usertypes.PromptMode.yesno, default=False, - abort_on=abort_on) + abort_on=abort_on, url=urlstr) if ignore is None: # prompt aborted ignore = False @@ -204,15 +210,17 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on): config_val = config.instance.get(option) if config_val == 'ask': if url.isValid(): + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) text = "Allow the website at {} to {}?".format( html.escape(url.toDisplayString()), msg) else: + urlstr = None text = "Allow the website to {}?".format(msg) return message.confirm_async( yes_action=yes_action, no_action=no_action, cancel_action=no_action, abort_on=abort_on, - title='Permission request', text=text) + title='Permission request', text=text, url=urlstr) elif config_val: yes_action() return None diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index 2d357a8e8..d2f563bb6 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -161,7 +161,7 @@ class QuickmarkManager(UrlMarkManager): "Add quickmark:", usertypes.PromptMode.text, functools.partial(self.quickmark_add, urlstr), text="Please enter a quickmark name for
{}".format( - html.escape(url.toDisplayString()))) + html.escape(url.toDisplayString())), url=urlstr) @cmdutils.register(instance='quickmark-manager') def quickmark_add(self, url, name): @@ -192,7 +192,7 @@ class QuickmarkManager(UrlMarkManager): if name in self.marks: message.confirm_async( title="Override existing quickmark?", - yes_action=set_mark, default=True) + yes_action=set_mark, default=True, url=url) else: set_mark() diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py index f5b6529d7..7c702a56f 100644 --- a/qutebrowser/browser/webengine/webenginedownloads.py +++ b/qutebrowser/browser/webengine/webenginedownloads.py @@ -135,6 +135,7 @@ class DownloadItem(downloads.AbstractDownloadItem): question = usertypes.Question() question.title = title question.text = msg + question.url = 'file://{}'.format(self._filename) question.mode = usertypes.PromptMode.yesno question.answered_yes.connect(self._after_set_filename) question.answered_no.connect(no_action) @@ -149,6 +150,7 @@ class DownloadItem(downloads.AbstractDownloadItem): question = usertypes.Question() question.title = title question.text = msg + question.url = 'file://{}'.format(os.path.dirname(self._filename)) question.mode = usertypes.PromptMode.yesno question.answered_yes.connect(lambda: self._after_create_parent_question( diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index f8ad7e2cc..280c55c39 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -757,10 +757,11 @@ class WebEngineTab(browsertab.AbstractTab): """Called when a proxy needs authentication.""" msg = "{} requires a username and password.".format( html_utils.escape(proxy_host)) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) answer = message.ask( title="Proxy authentication required", text=msg, mode=usertypes.PromptMode.user_pwd, - abort_on=[self.shutting_down, self.load_started]) + abort_on=[self.shutting_down, self.load_started], url=urlstr) if answer is not None: authenticator.setUser(answer.user) authenticator.setPassword(answer.password) diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 5c2bb4111..7a13bb07e 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -147,7 +147,8 @@ class BrowserPage(QWebPage): title="Open external application for {}-link?".format(scheme), text="URL: {}".format( html.escape(url.toDisplayString())), - yes_action=functools.partial(QDesktopServices.openUrl, url)) + yes_action=functools.partial(QDesktopServices.openUrl, url), + url=info.url.toString(QUrl.FullyEncoded)) return True elif (info.domain, info.error) in ignored_errors: log.webview.debug("Ignored error on {}: {} (error domain: {}, " diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index ff67cc414..72450978b 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2360,6 +2360,8 @@ bindings.default: : prompt-item-focus prev : prompt-item-focus next : prompt-item-focus next + : prompt-yank + : prompt-yank --sel : rl-backward-char : rl-forward-char : rl-backward-word @@ -2371,9 +2373,9 @@ bindings.default: : rl-kill-word : rl-unix-word-rubout : rl-backward-kill-word - : rl-yank : rl-delete-char : rl-backward-delete-char + : rl-yank : leave-mode caret: v: toggle-selection diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index df4c45096..931d32654 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -422,6 +422,27 @@ class PromptContainer(QWidget): except UnsupportedOperationError: pass + @cmdutils.register( + instance='prompt-container', scope='window', + modes=[usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]) + def prompt_yank(self, sel=False): + """Yank URL to clipboard or primary selection. + + Args: + sel: Use the primary selection instead of the clipboard. + """ + 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)) + class LineEdit(QLineEdit): @@ -721,6 +742,7 @@ class DownloadFilenamePrompt(FilenamePrompt): ('prompt-accept', 'Accept'), ('leave-mode', 'Abort'), ('prompt-open-download', "Open download"), + ('prompt-yank', "Yank URL"), ] return cmds @@ -811,6 +833,7 @@ class YesNoPrompt(_BasePrompt): cmds = [ ('prompt-accept yes', "Yes"), ('prompt-accept no', "No"), + ('prompt-yank', "Yank URL"), ] if self.question.default is not None: diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 49fb021a4..9dc8d7411 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -87,7 +87,8 @@ def info(message, *, replace=False): global_bridge.show(usertypes.MessageLevel.info, message, replace) -def _build_question(title, text=None, *, mode, default=None, abort_on=()): +def _build_question(title, text=None, *, mode, default=None, abort_on=(), + url=None): """Common function for ask/ask_async.""" if not isinstance(mode, usertypes.PromptMode): raise TypeError("Mode {} is no PromptMode member!".format(mode)) @@ -96,6 +97,7 @@ def _build_question(title, text=None, *, mode, default=None, abort_on=()): question.text = text question.mode = mode question.default = default + question.url = url for sig in abort_on: sig.connect(question.abort) return question diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index dc57e708c..8312cd803 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -266,6 +266,7 @@ class Question(QObject): For user_pwd, a default username as string. title: The question title to show. text: The prompt text to display to the user. + url: Any URL referenced in prompts. answer: The value the user entered (as password for user_pwd). is_aborted: Whether the question was aborted. interrupted: Whether the question was interrupted by another one. @@ -296,6 +297,7 @@ class Question(QObject): self.default = None self.title = None self.text = None + self.url = None self.answer = None self.is_aborted = False self.interrupted = False