Merge branch 'blyxxyz-clip'

This commit is contained in:
Florian Bruhin 2016-08-10 20:48:54 +02:00
commit 3d4fe5dde1
11 changed files with 172 additions and 91 deletions

View File

@ -30,6 +30,8 @@ Added
(to report bugs which are difficult to reproduce). (to report bugs which are difficult to reproduce).
- New `hide-unmatched-rapid-hints` option to not hide hint unmatched hint labels - New `hide-unmatched-rapid-hints` option to not hide hint unmatched hint labels
in rapid mode. in rapid mode.
- New `{clipboard}` and `{primary}` replacements for the commandline which
replace the `:paste` command.
Changed Changed
~~~~~~~ ~~~~~~~
@ -65,6 +67,12 @@ Changed
- The `:buffer` completion now also filters using the first column (id). - The `:buffer` completion now also filters using the first column (id).
- `:undo` has been improved to reopen tabs at the position they were closed. - `:undo` has been improved to reopen tabs at the position they were closed.
Deprecated
~~~~~~~~~~
- The `:paste` command got deprecated as `:open` with `{clipboard}` and
`{primary}` can be used instead.
Removed Removed
~~~~~~~ ~~~~~~~

View File

@ -145,12 +145,12 @@ Contributors, sorted by the number of commits in descending order:
* Antoni Boucher * Antoni Boucher
* Lamar Pavel * Lamar Pavel
* Bruno Oliveira * Bruno Oliveira
* Jan Verbeek
* Alexander Cogneau * Alexander Cogneau
* Marshall Lochbaum * Marshall Lochbaum
* Jakub Klinkovský * Jakub Klinkovský
* Felix Van der Jeugt * Felix Van der Jeugt
* Martin Tournoij * Martin Tournoij
* Jan Verbeek
* Raphael Pierzina * Raphael Pierzina
* Joel Torstensson * Joel Torstensson
* Patric Schmitz * Patric Schmitz

View File

@ -38,7 +38,6 @@
|<<messages,messages>>|Show a log of past messages. |<<messages,messages>>|Show a log of past messages.
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path. |<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|<<open,open>>|Open a URL in the current/[count]th tab. |<<open,open>>|Open a URL in the current/[count]th tab.
|<<paste,paste>>|Open a page from the clipboard.
|<<print,print>>|Print the current/[count]th tab. |<<print,print>>|Print the current/[count]th tab.
|<<quickmark-add,quickmark-add>>|Add a new quickmark. |<<quickmark-add,quickmark-add>>|Add a new quickmark.
|<<quickmark-del,quickmark-del>>|Delete a quickmark. |<<quickmark-del,quickmark-del>>|Delete a quickmark.
@ -487,6 +486,8 @@ Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] ['url']+
Open a URL in the current/[count]th tab. Open a URL in the current/[count]th tab.
If the URL contains newlines, each line gets opened in its own tab.
==== positional arguments ==== positional arguments
* +'url'+: The URL to open. * +'url'+: The URL to open.
@ -503,20 +504,6 @@ The tab index to open the URL in.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * 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]]
=== print === print
Syntax: +:print [*--preview*] [*--pdf* 'file']+ Syntax: +:print [*--preview*] [*--pdf* 'file']+

View File

@ -236,6 +236,8 @@ class CommandDispatcher:
bg=False, tab=False, window=False, count=None): bg=False, tab=False, window=False, count=None):
"""Open a URL in the current/[count]th tab. """Open a URL in the current/[count]th tab.
If the URL contains newlines, each line gets opened in its own tab.
Args: Args:
url: The URL to open. url: The URL to open.
bg: Open in a new background tab. bg: Open in a new background tab.
@ -247,35 +249,73 @@ class CommandDispatcher:
""" """
if url is None: if url is None:
if tab or bg or window: if tab or bg or window:
url = config.get('general', 'default-page') urls = [config.get('general', 'default-page')]
else: else:
raise cmdexc.CommandError("No URL given, but -t/-b/-w is not " raise cmdexc.CommandError("No URL given, but -t/-b/-w is not "
"set!") "set!")
else: else:
try: urls = self._parse_url_input(url)
url = objreg.get('quickmark-manager').get(url) for i, cur_url in enumerate(urls):
except urlmarks.Error: if not window and i > 0:
try: tab = False
url = urlutils.fuzzy_url(url) bg = True
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: if tab or bg or window:
self._open(url, tab, bg, window, not implicit) self._open(cur_url, tab, bg, window, not implicit)
else: else:
curtab = self._cntwidget(count) curtab = self._cntwidget(count)
if curtab is None: if curtab is None:
if count is None: if count is None:
# We want to open a URL in the current tab, but none exists # We want to open a URL in the current tab, but none
# yet. # exists yet.
self._tabbed_browser.tabopen(url) self._tabbed_browser.tabopen(cur_url)
else: else:
# Explicit count with a tab that doesn't exist. # Explicit count with a tab that doesn't exist.
return return
else: else:
curtab.openurl(url) 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', @cmdutils.register(instance='command-dispatcher', name='reload',
scope='window') scope='window')
@ -796,7 +836,8 @@ class CommandDispatcher:
else: else:
raise cmdexc.CommandError("Last tab") 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): def paste(self, sel=False, tab=False, bg=False, window=False):
"""Open a page from the clipboard. """Open a page from the clipboard.
@ -810,15 +851,12 @@ class CommandDispatcher:
window: Open in new window. window: Open in new window.
""" """
force_search = False force_search = False
if sel and utils.supports_selection(): if not utils.supports_selection():
target = "Primary selection"
else:
sel = False sel = False
target = "Clipboard" try:
text = utils.get_clipboard(selection=sel) text = utils.get_clipboard(selection=sel)
if not text.strip(): except utils.ClipboardError as e:
raise cmdexc.CommandError("{} is empty.".format(target)) raise cmdexc.CommandError(e)
log.misc.debug("{} contained: {!r}".format(target, text))
text_urls = [u for u in text.split('\n') if u.strip()] 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 if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and
urlutils.get_path_if_valid( urlutils.get_path_if_valid(
@ -1462,10 +1500,13 @@ class CommandDispatcher:
if not elem.is_editable(strict=True): if not elem.is_editable(strict=True):
raise cmdexc.CommandError("Focused element is not editable!") raise cmdexc.CommandError("Focused element is not editable!")
try:
try: try:
sel = utils.get_clipboard(selection=True) sel = utils.get_clipboard(selection=True)
except utils.SelectionUnsupportedError: except utils.SelectionUnsupportedError:
sel = utils.get_clipboard() sel = utils.get_clipboard()
except utils.ClipboardEmptyError:
return
log.misc.debug("Pasting primary selection into element {}".format( log.misc.debug("Pasting primary selection into element {}".format(
elem.debug_text())) elem.debug_text()))

View File

@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config, configexc from qutebrowser.config import config, configexc
from qutebrowser.commands import cmdexc, cmdutils 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 from qutebrowser.misc import split
@ -49,21 +49,29 @@ def _current_url(tabbed_browser):
def replace_variables(win_id, arglist): def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args.""" """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 = [] args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
if any('{url}' in arg for arg in arglist):
url = _current_url(tabbed_browser).toString(QUrl.FullyEncoded | try:
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: for arg in arglist:
if '{url}' in arg: for var, func in variables.items():
args.append(arg.replace('{url}', url)) if var in arg:
elif '{url:pretty}' in arg: if var not in values:
args.append(arg.replace('{url:pretty}', pretty_url)) values[var] = func()
else: arg = arg.replace(var, values[var])
args.append(arg) args.append(arg)
except utils.ClipboardError as e:
raise cmdexc.CommandError(e)
return args return args

View File

@ -1523,12 +1523,12 @@ KEY_DATA = collections.OrderedDict([
('yank domain -s', ['yD']), ('yank domain -s', ['yD']),
('yank pretty-url', ['yp']), ('yank pretty-url', ['yp']),
('yank pretty-url -s', ['yP']), ('yank pretty-url -s', ['yP']),
('paste', ['pp']), ('open {clipboard}', ['pp']),
('paste -s', ['pP']), ('open {primary}', ['pP']),
('paste -t', ['Pp']), ('open -t {clipboard}', ['Pp']),
('paste -ts', ['PP']), ('open -t {primary}', ['PP']),
('paste -w', ['wp']), ('open -w {clipboard}', ['wp']),
('paste -ws', ['wP']), ('open -w {primary}', ['wP']),
('quickmark-save', ['m']), ('quickmark-save', ['m']),
('set-cmd-text -s :quickmark-load', ['b']), ('set-cmd-text -s :quickmark-load', ['b']),
('set-cmd-text -s :quickmark-load -t', ['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 -p'), r'yank selection -s'),
(re.compile(r'^yank-selected'), r'yank selection'), (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-next'), r'completion-item-focus next'),
(re.compile(r'^completion-item-prev'), r'completion-item-focus prev'), (re.compile(r'^completion-item-prev'), r'completion-item-focus prev'),
] ]

View File

@ -47,7 +47,7 @@ class MinimalLineEditMixin:
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier: if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
try: try:
text = utils.get_clipboard(selection=True) text = utils.get_clipboard(selection=True)
except utils.SelectionUnsupportedError: except utils.ClipboardError:
pass pass
else: else:
e.accept() e.accept()

View File

@ -43,11 +43,21 @@ fake_clipboard = None
log_clipboard = False 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.""" """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): def elide(text, length):
"""Elide text so it uses a maximum of length chars.""" """Elide text so it uses a maximum of length chars."""
if length < 1: if length < 1:
@ -810,6 +820,11 @@ def get_clipboard(selection=False):
mode = QClipboard.Selection if selection else QClipboard.Clipboard mode = QClipboard.Selection if selection else QClipboard.Clipboard
data = QApplication.clipboard().text(mode=mode) 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 return data

View File

@ -524,3 +524,16 @@ Feature: Various utility commands.
Then the following tabs should be open: Then the following tabs should be open:
- data/hints/link_blank.html - data/hints/link_blank.html
- data/hello.txt (active) - 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

View File

@ -1,6 +1,6 @@
Feature: Yanking and pasting. Feature: Yanking and pasting.
:yank and :paste can be used to copy/paste the URL or title from/to the :yank, {clipboard} and {primary} can be used to copy/paste the URL or title
clipboard and primary selection. from/to the clipboard and primary selection.
Background: Background:
Given I run :tab-only 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 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" And the clipboard should contain "http://localhost:(port)/data/title with spaces.html"
#### :paste #### {clipboard} and {primary}
Scenario: Pasting a URL Scenario: Pasting a URL
When I put "http://localhost:(port)/data/hello.txt" into the clipboard 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 And I wait until data/hello.txt is loaded
Then the requests should be: Then the requests should be:
data/hello.txt data/hello.txt
@ -57,32 +57,32 @@ Feature: Yanking and pasting.
Scenario: Pasting a URL from primary selection Scenario: Pasting a URL from primary selection
When selection is supported When selection is supported
And I put "http://localhost:(port)/data/hello2.txt" into the primary selection 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 And I wait until data/hello2.txt is loaded
Then the requests should be: Then the requests should be:
data/hello2.txt data/hello2.txt
Scenario: Pasting with empty clipboard Scenario: Pasting with empty clipboard
When I put "" into the 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 Then the error "Clipboard is empty." should be shown
Scenario: Pasting with empty selection Scenario: Pasting with empty selection
When selection is supported When selection is supported
And I put "" into the primary selection 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 Then the error "Primary selection is empty." should be shown
Scenario: Pasting with a space in clipboard Scenario: Pasting with a space in clipboard
When I put " " into the 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 Then the error "Clipboard is empty." should be shown
Scenario: Pasting in a new tab Scenario: Pasting in a new tab
Given I open about:blank Given I open about:blank
When I run :tab-only When I run :tab-only
And I put "http://localhost:(port)/data/hello.txt" into the clipboard 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 And I wait until data/hello.txt is loaded
Then the following tabs should be open: Then the following tabs should be open:
- about:blank - about:blank
@ -92,7 +92,7 @@ Feature: Yanking and pasting.
Given I open about:blank Given I open about:blank
When I run :tab-only When I run :tab-only
And I put "http://localhost:(port)/data/hello.txt" into the clipboard 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 And I wait until data/hello.txt is loaded
Then the following tabs should be open: Then the following tabs should be open:
- about:blank (active) - about:blank (active)
@ -101,7 +101,7 @@ Feature: Yanking and pasting.
Scenario: Pasting in a new window Scenario: Pasting in a new window
Given I have a fresh instance Given I have a fresh instance
When I put "http://localhost:(port)/data/hello.txt" into the clipboard 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 And I wait until data/hello.txt is loaded
Then the session should look like: Then the session should look like:
windows: windows:
@ -119,7 +119,7 @@ Feature: Yanking and pasting.
Scenario: Pasting an invalid URL Scenario: Pasting an invalid URL
When I set general -> auto-search to false When I set general -> auto-search to false
And I put "foo bar" into the clipboard 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 Then the error "Invalid URL" should be shown
Scenario: Pasting multiple urls in a new tab 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/hello.txt
http://localhost:(port)/data/hello2.txt http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.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/hello.txt is loaded
And I wait until data/hello2.txt is loaded And I wait until data/hello2.txt is loaded
And I wait until data/hello3.txt is loaded And I wait until data/hello3.txt is loaded
@ -145,7 +145,7 @@ Feature: Yanking and pasting.
this url: this url:
http://qutebrowser.org http://qutebrowser.org
should not open 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 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: Then the following tabs should be open:
- about:blank - about:blank
@ -159,7 +159,7 @@ Feature: Yanking and pasting.
text: text:
should open should open
as search 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 And I wait until data/hello.txt?q=text%3A%0Ashould%20open%0Aas%20search is loaded
Then the following tabs should be open: Then the following tabs should be open:
- about:blank - about:blank
@ -172,7 +172,7 @@ Feature: Yanking and pasting.
http://localhost:(port)/data/hello.txt http://localhost:(port)/data/hello.txt
http://localhost:(port)/data/hello2.txt http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.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/hello.txt is loaded
And I wait until data/hello2.txt is loaded And I wait until data/hello2.txt is loaded
And I wait until data/hello3.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/hello.txt
http://localhost:(port)/data/hello2.txt http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.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/hello.txt is loaded
And I wait until data/hello2.txt is loaded And I wait until data/hello2.txt is loaded
And I wait until data/hello3.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 Scenario: Pasting multiple urls with an empty one
When I open about:blank 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 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 Then no crash should happen
Scenario: Pasting multiple urls with an almost empty one Scenario: Pasting multiple urls with an almost empty one
When I open about:blank 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 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 Then no crash should happen
#### :paste-primary #### :paste-primary

View File

@ -298,6 +298,10 @@ class TestKeyConfigParser:
('yank -ds', 'yank domain -s'), ('yank -ds', 'yank domain -s'),
('yank -p', 'yank pretty-url'), ('yank -p', 'yank pretty-url'),
('yank -ps', 'yank pretty-url -s'), ('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): def test_migrations(self, old, new_expected):