diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 90b5455d5..4a90783f8 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -38,7 +38,6 @@ |<>|Show a log of past messages. |<>|Open typical prev/next links or navigate using the URL path. |<>|Open a URL in the current/[count]th tab. -|<>|Open a page from the clipboard. |<>|Print the current/[count]th tab. |<>|Add a new quickmark. |<>|Delete a quickmark. @@ -487,6 +486,8 @@ Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] ['url']+ Open a URL in the current/[count]th tab. +If the URL contains newlines, each line gets opened in its own tab. + ==== positional arguments * +'url'+: The URL to open. @@ -503,20 +504,6 @@ The tab index to open the URL in. ==== note * This command does not split arguments after the last argument and handles quotes literally. -[[paste]] -=== paste -Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+ - -Open a page from the clipboard. - -If the pasted text contains newlines, each line gets opened in its own tab. - -==== optional arguments -* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. -* +*-t*+, +*--tab*+: Open in a new tab. -* +*-b*+, +*--bg*+: Open in a background tab. -* +*-w*+, +*--window*+: Open in new window. - [[print]] === print Syntax: +:print [*--preview*] [*--pdf* 'file']+ diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 12f513e50..699546c6e 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -236,6 +236,8 @@ class CommandDispatcher: bg=False, tab=False, window=False, count=None): """Open a URL in the current/[count]th tab. + If the URL contains newlines, each line gets opened in its own tab. + Args: url: The URL to open. bg: Open in a new background tab. @@ -247,35 +249,73 @@ class CommandDispatcher: """ if url is None: if tab or bg or window: - url = config.get('general', 'default-page') + urls = [config.get('general', 'default-page')] else: raise cmdexc.CommandError("No URL given, but -t/-b/-w is not " "set!") else: - try: - url = objreg.get('quickmark-manager').get(url) - except urlmarks.Error: - try: - url = urlutils.fuzzy_url(url) - except urlutils.InvalidUrlError as e: - # We don't use cmdexc.CommandError here as this can be - # called async from edit_url - message.error(self._win_id, str(e)) - return - if tab or bg or window: - self._open(url, tab, bg, window, not implicit) - else: - curtab = self._cntwidget(count) - if curtab is None: - if count is None: - # We want to open a URL in the current tab, but none exists - # yet. - self._tabbed_browser.tabopen(url) - else: - # Explicit count with a tab that doesn't exist. - return + urls = self._parse_url_input(url) + for i, cur_url in enumerate(urls): + if not window and i > 0: + tab = False + bg = True + if tab or bg or window: + self._open(cur_url, tab, bg, window, not implicit) else: - curtab.openurl(url) + curtab = self._cntwidget(count) + if curtab is None: + if count is None: + # We want to open a URL in the current tab, but none + # exists yet. + self._tabbed_browser.tabopen(cur_url) + else: + # Explicit count with a tab that doesn't exist. + return + else: + curtab.openurl(cur_url) + + def _parse_url(self, url, *, force_search=False): + """Parse a URL or quickmark or search query. + + Args: + url: The URL to parse. + force_search: Whether to force a search even if the content can be + interpreted as a URL or a path. + + Return: + A URL that can be opened. + """ + try: + return objreg.get('quickmark-manager').get(url) + except urlmarks.Error: + try: + return urlutils.fuzzy_url(url, force_search=force_search) + except urlutils.InvalidUrlError as e: + # We don't use cmdexc.CommandError here as this can be + # called async from edit_url + message.error(self._win_id, str(e)) + return None + + def _parse_url_input(self, url): + """Parse a URL or newline-separated list of URLs. + + Args: + url: The URL or list to parse. + + Return: + A list of URLs that can be opened. + """ + force_search = False + urllist = [u for u in url.split('\n') if u.strip()] + if (len(urllist) > 1 and not urlutils.is_url(urllist[0]) and + urlutils.get_path_if_valid(urllist[0], check_exists=True) + is None): + urllist = [url] + force_search = True + for cur_url in urllist: + parsed = self._parse_url(cur_url, force_search=force_search) + if parsed is not None: + yield parsed @cmdutils.register(instance='command-dispatcher', name='reload', scope='window') @@ -796,7 +836,8 @@ class CommandDispatcher: else: raise cmdexc.CommandError("Last tab") - @cmdutils.register(instance='command-dispatcher', scope='window') + @cmdutils.register(instance='command-dispatcher', scope='window', + deprecated="Use :open {clipboard}") def paste(self, sel=False, tab=False, bg=False, window=False): """Open a page from the clipboard. @@ -810,15 +851,12 @@ class CommandDispatcher: window: Open in new window. """ force_search = False - if sel and utils.supports_selection(): - target = "Primary selection" - else: + if not utils.supports_selection(): sel = False - target = "Clipboard" - text = utils.get_clipboard(selection=sel) - if not text.strip(): - raise cmdexc.CommandError("{} is empty.".format(target)) - log.misc.debug("{} contained: {!r}".format(target, text)) + try: + text = utils.get_clipboard(selection=sel) + except utils.ClipboardError as e: + raise cmdexc.CommandError(e) text_urls = [u for u in text.split('\n') if u.strip()] if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and urlutils.get_path_if_valid( @@ -1463,9 +1501,12 @@ class CommandDispatcher: raise cmdexc.CommandError("Focused element is not editable!") try: - sel = utils.get_clipboard(selection=True) - except utils.SelectionUnsupportedError: - sel = utils.get_clipboard() + try: + sel = utils.get_clipboard(selection=True) + except utils.SelectionUnsupportedError: + sel = utils.get_clipboard() + except utils.ClipboardEmptyError: + return log.misc.debug("Pasting primary selection into element {}".format( elem.debug_text())) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 323513604..6a132598d 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject from qutebrowser.config import config, configexc from qutebrowser.commands import cmdexc, cmdutils -from qutebrowser.utils import message, objreg, qtutils +from qutebrowser.utils import message, objreg, qtutils, utils from qutebrowser.misc import split @@ -49,21 +49,29 @@ def _current_url(tabbed_browser): def replace_variables(win_id, arglist): """Utility function to replace variables like {url} in a list of args.""" + variables = { + '{url}': lambda: _current_url(tabbed_browser).toString( + QUrl.FullyEncoded | QUrl.RemovePassword), + '{url:pretty}': lambda: _current_url(tabbed_browser).toString( + QUrl.RemovePassword), + '{clipboard}': utils.get_clipboard, + '{primary}': lambda: utils.get_clipboard(selection=True), + } + values = {} args = [] tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - if any('{url}' in arg for arg in arglist): - url = _current_url(tabbed_browser).toString(QUrl.FullyEncoded | - QUrl.RemovePassword) - if any('{url:pretty}' in arg for arg in arglist): - pretty_url = _current_url(tabbed_browser).toString(QUrl.RemovePassword) - for arg in arglist: - if '{url}' in arg: - args.append(arg.replace('{url}', url)) - elif '{url:pretty}' in arg: - args.append(arg.replace('{url:pretty}', pretty_url)) - else: + + try: + for arg in arglist: + for var, func in variables.items(): + if var in arg: + if var not in values: + values[var] = func() + arg = arg.replace(var, values[var]) args.append(arg) + except utils.ClipboardError as e: + raise cmdexc.CommandError(e) return args diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 656b8c1ab..b62968904 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1523,12 +1523,12 @@ KEY_DATA = collections.OrderedDict([ ('yank domain -s', ['yD']), ('yank pretty-url', ['yp']), ('yank pretty-url -s', ['yP']), - ('paste', ['pp']), - ('paste -s', ['pP']), - ('paste -t', ['Pp']), - ('paste -ts', ['PP']), - ('paste -w', ['wp']), - ('paste -ws', ['wP']), + ('open {clipboard}', ['pp']), + ('open {primary}', ['pP']), + ('open -t {clipboard}', ['Pp']), + ('open -t {primary}', ['PP']), + ('open -w {clipboard}', ['wp']), + ('open -w {primary}', ['wP']), ('quickmark-save', ['m']), ('set-cmd-text -s :quickmark-load', ['b']), ('set-cmd-text -s :quickmark-load -t', ['B']), @@ -1698,6 +1698,11 @@ CHANGED_KEY_COMMANDS = [ (re.compile(r'^yank-selected -p'), r'yank selection -s'), (re.compile(r'^yank-selected'), r'yank selection'), + (re.compile(r'^paste$'), r'open {clipboard}'), + (re.compile(r'^paste -([twb])$'), r'open -\1 {clipboard}'), + (re.compile(r'^paste -([twb])s$'), r'open -\1 {primary}'), + (re.compile(r'^paste -s([twb])$'), r'open -\1 {primary}'), + (re.compile(r'^completion-item-next'), r'completion-item-focus next'), (re.compile(r'^completion-item-prev'), r'completion-item-focus prev'), ] diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index 63a5718c6..ba21e541a 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -47,7 +47,7 @@ class MinimalLineEditMixin: if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier: try: text = utils.get_clipboard(selection=True) - except utils.SelectionUnsupportedError: + except utils.ClipboardError: pass else: e.accept() diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index fd8419d12..72d6f83e2 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -43,11 +43,21 @@ fake_clipboard = None log_clipboard = False -class SelectionUnsupportedError(Exception): +class ClipboardError(Exception): + + """Raised if the clipboard contents are unavailable for some reason.""" + + +class SelectionUnsupportedError(ClipboardError): """Raised if [gs]et_clipboard is used and selection=True is unsupported.""" +class ClipboardEmptyError(ClipboardError): + + """Raised if get_clipboard is used and the clipboard is empty.""" + + def elide(text, length): """Elide text so it uses a maximum of length chars.""" if length < 1: @@ -810,6 +820,11 @@ def get_clipboard(selection=False): mode = QClipboard.Selection if selection else QClipboard.Clipboard data = QApplication.clipboard().text(mode=mode) + target = "Primary selection" if selection else "Clipboard" + if not data.strip(): + raise ClipboardEmptyError("{} is empty.".format(target)) + log.misc.debug("{} contained: {!r}".format(target, data)) + return data diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 686f5a8b4..fa20d26ac 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -524,3 +524,16 @@ Feature: Various utility commands. Then the following tabs should be open: - data/hints/link_blank.html - data/hello.txt (active) + + ## Variables + + Scenario: {url} as part of an argument + When I open data/hello.txt + And I run :message-info foo{url} + Then the message "foohttp://localhost:*/hello.txt" should be shown + + Scenario: Multiple variables in an argument + When I open data/hello.txt + And I put "foo" into the clipboard + And I run :message-info {clipboard}bar{url} + Then the message "foobarhttp://localhost:*/hello.txt" should be shown diff --git a/tests/end2end/features/yankpaste.feature b/tests/end2end/features/yankpaste.feature index a8aae1b90..c453b2e42 100644 --- a/tests/end2end/features/yankpaste.feature +++ b/tests/end2end/features/yankpaste.feature @@ -1,6 +1,6 @@ Feature: Yanking and pasting. - :yank and :paste can be used to copy/paste the URL or title from/to the - clipboard and primary selection. + :yank, {clipboard} and {primary} can be used to copy/paste the URL or title + from/to the clipboard and primary selection. Background: Given I run :tab-only @@ -45,11 +45,11 @@ Feature: Yanking and pasting. Then the message "Yanked URL to clipboard: http://localhost:(port)/data/title with spaces.html" should be shown And the clipboard should contain "http://localhost:(port)/data/title with spaces.html" - #### :paste + #### {clipboard} and {primary} Scenario: Pasting a URL When I put "http://localhost:(port)/data/hello.txt" into the clipboard - And I run :paste + And I run :open {clipboard} And I wait until data/hello.txt is loaded Then the requests should be: data/hello.txt @@ -57,32 +57,32 @@ Feature: Yanking and pasting. Scenario: Pasting a URL from primary selection When selection is supported And I put "http://localhost:(port)/data/hello2.txt" into the primary selection - And I run :paste --sel + And I run :open {primary} And I wait until data/hello2.txt is loaded Then the requests should be: data/hello2.txt Scenario: Pasting with empty clipboard When I put "" into the clipboard - And I run :paste + And I run :open {clipboard} (invalid command) Then the error "Clipboard is empty." should be shown Scenario: Pasting with empty selection When selection is supported And I put "" into the primary selection - And I run :paste --sel + And I run :open {primary} (invalid command) Then the error "Primary selection is empty." should be shown Scenario: Pasting with a space in clipboard When I put " " into the clipboard - And I run :paste + And I run :open {clipboard} (invalid command) Then the error "Clipboard is empty." should be shown Scenario: Pasting in a new tab Given I open about:blank When I run :tab-only And I put "http://localhost:(port)/data/hello.txt" into the clipboard - And I run :paste -t + And I run :open -t {clipboard} And I wait until data/hello.txt is loaded Then the following tabs should be open: - about:blank @@ -92,7 +92,7 @@ Feature: Yanking and pasting. Given I open about:blank When I run :tab-only And I put "http://localhost:(port)/data/hello.txt" into the clipboard - And I run :paste -b + And I run :open -b {clipboard} And I wait until data/hello.txt is loaded Then the following tabs should be open: - about:blank (active) @@ -101,7 +101,7 @@ Feature: Yanking and pasting. Scenario: Pasting in a new window Given I have a fresh instance When I put "http://localhost:(port)/data/hello.txt" into the clipboard - And I run :paste -w + And I run :open -w {clipboard} And I wait until data/hello.txt is loaded Then the session should look like: windows: @@ -119,7 +119,7 @@ Feature: Yanking and pasting. Scenario: Pasting an invalid URL When I set general -> auto-search to false And I put "foo bar" into the clipboard - And I run :paste + And I run :open {clipboard} Then the error "Invalid URL" should be shown Scenario: Pasting multiple urls in a new tab @@ -128,7 +128,7 @@ Feature: Yanking and pasting. http://localhost:(port)/data/hello.txt http://localhost:(port)/data/hello2.txt http://localhost:(port)/data/hello3.txt - And I run :paste -t + And I run :open -t {clipboard} And I wait until data/hello.txt is loaded And I wait until data/hello2.txt is loaded And I wait until data/hello3.txt is loaded @@ -145,7 +145,7 @@ Feature: Yanking and pasting. this url: http://qutebrowser.org should not open - And I run :paste -t + And I run :open -t {clipboard} And I wait until data/hello.txt?q=this%20url%3A%0Ahttp%3A//qutebrowser.org%0Ashould%20not%20open is loaded Then the following tabs should be open: - about:blank @@ -159,7 +159,7 @@ Feature: Yanking and pasting. text: should open as search - And I run :paste -t + And I run :open -t {clipboard} And I wait until data/hello.txt?q=text%3A%0Ashould%20open%0Aas%20search is loaded Then the following tabs should be open: - about:blank @@ -172,7 +172,7 @@ Feature: Yanking and pasting. http://localhost:(port)/data/hello.txt http://localhost:(port)/data/hello2.txt http://localhost:(port)/data/hello3.txt - And I run :paste -b + And I run :open -b {clipboard} And I wait until data/hello.txt is loaded And I wait until data/hello2.txt is loaded And I wait until data/hello3.txt is loaded @@ -188,7 +188,7 @@ Feature: Yanking and pasting. http://localhost:(port)/data/hello.txt http://localhost:(port)/data/hello2.txt http://localhost:(port)/data/hello3.txt - And I run :paste -w + And I run :open -w {clipboard} And I wait until data/hello.txt is loaded And I wait until data/hello2.txt is loaded And I wait until data/hello3.txt is loaded @@ -218,13 +218,13 @@ Feature: Yanking and pasting. Scenario: Pasting multiple urls with an empty one When I open about:blank And I put "http://localhost:(port)/data/hello.txt\n\nhttp://localhost:(port)/data/hello2.txt" into the clipboard - And I run :paste -t + And I run :open -t {clipboard} Then no crash should happen Scenario: Pasting multiple urls with an almost empty one When I open about:blank And I put "http://localhost:(port)/data/hello.txt\n \nhttp://localhost:(port)/data/hello2.txt" into the clipboard - And I run :paste -t + And I run :open -t {clipboard} Then no crash should happen #### :paste-primary diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 1d22ed0fe..6ef44f47d 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -298,6 +298,10 @@ class TestKeyConfigParser: ('yank -ds', 'yank domain -s'), ('yank -p', 'yank pretty-url'), ('yank -ps', 'yank pretty-url -s'), + + ('paste', 'open {clipboard}'), + ('paste -t', 'open -t {clipboard}'), + ('paste -ws', 'open -w {primary}'), ] ) def test_migrations(self, old, new_expected):