From c40b08562a00772ebe3b21fb71f0f4ff78ebab7c Mon Sep 17 00:00:00 2001 From: Mladen Mijatov Date: Tue, 16 Jun 2020 21:54:53 +0200 Subject: [PATCH] Clipboard: Added command line provider. --- sunflower/clipboard.py | 92 ++++++++++++++++++++++++++++++ sunflower/gui/main_window.py | 42 +++++++++----- sunflower/plugin_base/item_list.py | 8 +-- 3 files changed, 124 insertions(+), 18 deletions(-) diff --git a/sunflower/clipboard.py b/sunflower/clipboard.py index 2b5c76742..5f5ad3fba 100644 --- a/sunflower/clipboard.py +++ b/sunflower/clipboard.py @@ -1,6 +1,9 @@ import sys from gi.repository import Gtk, Gdk +from subprocess import check_output, run +from threading import Thread +from sunflower.common import executable_exists class Clipboard: @@ -12,6 +15,7 @@ def __init__(self): self.data_support = [] self.add_provider(GtkProvider()) + self.add_provider(CommandProvider()) self.add_provider(FakeProvider()) def add_provider(self, provider): @@ -205,3 +209,91 @@ def data_available(self, mime_types): """Check if clipboard with specified mime types is available.""" targets_available = [target in self.data for target in mime_types] return any(targets_available) + + +class CommandProvider(Provider): + """Clipboard integration using command line application and piping.""" + + def __init__(self): + commands = ['xclip', 'wl-copy', 'wl-paste'] + self.available_commands = tuple(filter(lambda command: executable_exists(command), commands)) + + def available(self): + """Test environment and return tuple of boolean values indicating usability.""" + result = len(self.available_commands) > 0 + return result, result + + def set_text(self, text): + """Set text content.""" + commands = ( + ('wl-copy',), + ('xclip', '-selection', 'clipboard'), + ) + + for command in commands: + try: + run(command, input=text, text=True) + except: + pass + else: + break + + def set_data(self, data, mime_types): + """Set data as content with provided list of mime types.""" + commands = ( + ('wl-copy', '-t', mime_types[0]), + ('xclip', '-selection', 'clipboard', '-t', mime_types[0]), + ) + + for command in commands: + try: + run(command, input=data, text=True) + except: + pass + else: + break + + def get_text(self): + """Return text value stored in clipboard.""" + result = None + commands = ( + ('wl-paste',), + ('xclip', '-selection', 'clipboard', '-o'), + ) + + for command in commands: + try: + result = check_output(command).decode('unicode-escape') + except: + pass + else: + break + + return result + + def get_data(self, mime_types): + """Return data stored for provided types in clipboard.""" + result = None + commands = ( + ('wl-paste', '-t', mime_types[0]), + ('xclip', '-selection', 'clipboard', '-o', '-t', mime_types[0]), + ) + + for command in commands: + try: + result = check_output(command, input=data, text=True).decode('unicode-escape') + except: + pass + else: + break + + return result + + def text_available(self): + """Check if clipboard with text is available.""" + return self.get_text() is not None + + def data_available(self, mime_types): + """Check if clipboard with specified mime types is available.""" + return self.get_data(mime_types) is not None + diff --git a/sunflower/gui/main_window.py b/sunflower/gui/main_window.py index c302748da..377484703 100644 --- a/sunflower/gui/main_window.py +++ b/sunflower/gui/main_window.py @@ -2368,15 +2368,10 @@ def set_clipboard_text(self, text): self.clipboard.set_text(text) def set_clipboard_item_list(self, operation, uri_list): - """Set clipboard to contain list of items - - operation - 'copy' or 'cut' string representing operation - uri_list - list of URIs - - """ - targets = ['x-special/gnome-copied-files', 'text/uri-list'] - raw_data = '{0}\n'.format(operation) + '\n'.join(uri_list) - return self.clipboard.set_data(raw_data, targets) + """Set clipboard to contain list of items for either `copy` or `cut` operation.""" + targets = ['x-special/nautilus-clipboard', 'x-special/gnome-copied-files', 'text/uri-list'] + raw_data = '{}\n{}\n{}\n'.format(targets[0], operation, '\n'.join(uri_list)) + return self.clipboard.set_text(raw_data) def get_clipboard_text(self): """Get text from clipboard""" @@ -2385,14 +2380,26 @@ def get_clipboard_text(self): def get_clipboard_item_list(self): """Get item list from clipboard""" result = None - targets = ['x-special/gnome-copied-files', 'text/uri-list'] - selection = self.clipboard.get_data(targets) + targets = ['x-special/nautilus-clipboard', 'x-special/gnome-copied-files', 'text/uri-list'] - # in case there is something to paste + # nautilus recently provides data through regular + # clipboard as plain text try getting data that way + selection = self.clipboard.get_text() if selection is not None: data = selection.splitlines(False) + data = list(filter(lambda x: len(x) > 0, data)) + if data[0] in targets: + data.pop(0) result = (data[0], data[1:]) + # try getting data old way through mime type targets + else: + selection = self.clipboard.get_data(targets) + if selection is not None: + data = selection.splitlines(False) + data = list(filter(lambda x: len(x) > 0, data)) + result = (data[0], data[1:]) + return result def is_clipboard_text(self): @@ -2401,8 +2408,15 @@ def is_clipboard_text(self): def is_clipboard_item_list(self): """Check if clipboard data is URI list""" - targets = ['x-special/gnome-copied-files', 'text/uri-list'] - return self.clipboard.data_available(targets) + targets = ['x-special/nautilus-clipboard', 'x-special/gnome-copied-files', 'text/uri-list'] + result = False + + selection = self.clipboard.get_text() + if selection is not None: + data = selection.splitlines(False) + result = data[0] == targets[0] or data[0] in ('copy', 'cut') + + return result def is_archive_supported(self, mime_type): """Check if specified archive mime type is supported.""" diff --git a/sunflower/plugin_base/item_list.py b/sunflower/plugin_base/item_list.py index f5f9adbf5..545d94c57 100644 --- a/sunflower/plugin_base/item_list.py +++ b/sunflower/plugin_base/item_list.py @@ -888,14 +888,14 @@ def _paste_files_from_clipboard(self, widget=None, data=None): # clipboard data contains URI list if data is not None: operation = data[0] - list_ = data[1] - protocol = list_[0].split('://')[0] + uri_list = data[1] + protocol = uri_list[0].split('://')[0] # convert URI to normal path - list_ = [urllib.parse.unquote(item.split('://')[1]) for item in list_] + uri_list = [urllib.parse.unquote(item.split('://')[1]) for item in uri_list] # call handler - self._handle_external_data(operation, protocol, list_, self.path) + self._handle_external_data(operation, protocol, uri_list, self.path) return True