Repository: saulpw/visidata

Issue:
Portable clipboard handling
Currently, copying to the system clipboard must be configured manually for every platform apart from macOS. Although it's documented (e.g. [here](http://visidata.org/man/)), I assumed the option was only needed for advanced customization (e.g. on non-standard platforms), and didn't realize that it must be set on Linux until it errored when I tried to use it (and I saw it mentioned in a [comment on a closed issue](https://github.com/saulpw/visidata/issues/98#issuecomment-379616663)).

Why not set it up automatically on all systems, rather than just Macs? There's a library which supports writing to and reading from the clipboard 
portably and painlessly
: [pyperclip](https://github.com/asweigart/pyperclip). It could either be added as an optional dependency, or — better still — copy 'n' pasted into the project (it's a [single file](https://github.com/asweigart/pyperclip/commit/df268c11d1b9bdfcb322e7fdcb61191e572e316c)). The current `options.clipboard_copy_cmd` option could be kept for advanced usage/non-standard platforms.


Discussion:
Hi @chocolateboy, thanks for the feedback.  Around the time of that comment, I tried including pyperclip as you suggest but had issues getting it to work on my (Linux) computer.  I don't remember the details but I think it was going to require more hackery than I was up for at the time.  If you have the time, I'd love if you gave it a go!  If you get it working (it may be easier than I thought), please submit a PR against the dev branch and we'll be happy to include it in 1.3!

Resolving patch:
diff --git a/visidata/clipboard.py b/visidata/clipboard.py
--- a/visidata/clipboard.py
+++ b/visidata/clipboard.py
@@ -1,3 +1,7 @@
+import shutil
+import subprocess
+import sys
+import tempfile
 
 from visidata import *
 
@@ -20,35 +24,78 @@
 globalCommand('gY', 'saveToClipboard(sheet, selectedRows or rows, input("copy rows to system clipboard as filetype: ", value=options.filetype or "csv"))', 'yank (copy) selected rows to system clipboard', 'data-clipboard-copy-system-selected')
 globalCommand('zY', 'copyToClipboard(cursorDisplay)', 'yank (copy) current cell to system clipboard', 'data-clipboard-copy-system-cell')
 
-option('clipboard_copy_cmd', 'pbcopy w', 'command to copy stdin to system clipboard')
-# or 'xsel --primary' for xsel
-# or 'xclip -selection primary'
-# or 'pbcopy w' for mac
+option('clipboard_copy_cmd', '', 'command to copy stdin to system clipboard')
 
-import tempfile
-import subprocess
+commands = {
+    ('clip', 'win32'):    [],                                      # Windows Vista+
+    ('pbcopy', 'darwin'): ['w'],                                   # macOS
+    ('xclip', None):      ['-selection', 'clipboard', '-filter'],  # Linux etc.
+    ('xsel', None):       ['--clipboard', '--input'],              # Linux etc.
+}
+
+
+class _Clipboard:
+    'Cross-platform helper to copy a cell or multiple rows to the system clipboard.'
+
+    def __init__(self):
+        self._command = options.clipboard_copy_cmd or self.__default_command()
+
+    def __default_command(self):
+        for (command, platform), options in commands.items():
+            if platform is None or sys.platform == platform:
+                path = shutil.which(command)
+                if path:
+                    return [path] + options
+
+    def copy(self, value):
+        'Copy a cell to the system clipboard.'
+        if not self._command:
+            error('options.clipboard_copy_cmd not set')
 
-def copyToClipboard(val):
-    cmd = options.clipboard_copy_cmd or error('options.clipboard_copy_cmd not set')
-    with tempfile.NamedTemporaryFile() as temp:
-        with open(temp.name, 'w', encoding=options.encoding) as fp:
-            fp.write(str(val))
+        with tempfile.NamedTemporaryFile() as temp:
+            with open(temp.name, 'w', encoding=options.encoding) as fp:
+                fp.write(str(value))
 
-        p = subprocess.Popen(cmd.split(), stdin=open(temp.name, 'r', encoding=options.encoding))
-        p.communicate()
+            p = subprocess.Popen(
+                self._command,
+                stdin=open(temp.name, 'r', encoding=options.encoding),
+                stdout=subprocess.DEVNULL)
+            p.communicate()
+
+    def save(self, vs, filetype):
+        'Copy rows to the system clipboard.'
+        if not self._command:
+            error('options.clipboard_copy_cmd not set')
+
+        with tempfile.NamedTemporaryFile() as temp:
+            tempfn = temp.name + "." + filetype
+            saveSheets(tempfn, vs)
+            sync(1)
+            p = subprocess.Popen(
+                self._command,
+                stdin=open(tempfn, 'r', encoding=options.encoding),
+                stdout=subprocess.DEVNULL,
+                close_fds=True)
+            p.communicate()
+
+
+clipboard = None
+
+
+def copyToClipboard(value):
+    global clipboard
+    clipboard = clipboard or _Clipboard()
+    clipboard.copy(value)
     status('copied value to clipboard')
 
+
 @async
 def saveToClipboard(sheet, rows, filetype=None):
-    cmd = options.clipboard_copy_cmd or error('options.clipboard_copy_cmd not set')
+    global clipboard
+    clipboard = clipboard or _Clipboard()
+    filetype = filetype or options.filetype
     vs = copy(sheet)
     vs.rows = rows
-    filetype = filetype or options.filetype
-    with tempfile.NamedTemporaryFile() as temp:
-        tempfn = temp.name + "." + filetype
-        status('copying rows to clipboard')
-        saveSheets(tempfn, vs)
-        sync(1)
-        p = subprocess.Popen(cmd.split(), stdin=open(tempfn, 'r', encoding=options.encoding), close_fds=True)
-        p.communicate()
+    status('copying rows to clipboard')
+    clipboard.save(vs, filetype)
     status('done')


Test patch: