Skip to content

Instantly share code, notes, and snippets.

@jclosure
Last active February 8, 2023 21:01
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save jclosure/19698429dda1105b8a93b0832c07ebc7 to your computer and use it in GitHub Desktop.
Save jclosure/19698429dda1105b8a93b0832c07ebc7 to your computer and use it in GitHub Desktop.
Setup a bi-directional shared clipboard between client macos and linux server for remote terminal emacs.

Using netcat and ssh tunnels, create a shared clipboard allowing client and server clipboards to be fused

Serve pbcopy and pbpaste on Mac's localhost

NOTE: That we are making the assumption that the clipboards are sending and receiving UTF-8 encoded bytes

~/Library/LaunchAgents/pbcopy.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
	<key>Label</key>
	<string>localhost.pbcopy</string>
	<key>ProgramArguments</key>
	<array>
	    <string>/bin/sh</string>
	    <string>-c</string>
	    <string>LC_CTYPE=en_US.UTF-8 pbcopy</string>
	</array>
	<key>Sockets</key>
	<dict>
	    <key>Listeners</key>
	    <dict>
		<key>SockNodeName</key>
		<string>127.0.0.1</string>
		<key>SockServiceName</key>
		<string>2224</string>
	    </dict>
	</dict>
	<key>inetdCompatibility</key>
	<dict>
	    <key>Wait</key>
	    <false/>
	</dict>
    </dict>
</plist>

~/Library/LaunchAgents/pbpaste.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
	<key>Label</key>
	<string>localhost.pbpaste</string>
	<key>ProgramArguments</key>
	<array>
	    <string>/bin/sh</string>
	    <string>-c</string>
	    <string>LC_CTYPE=en_US.UTF-8 pbpaste</string>
	</array>
	<key>Sockets</key>
	<dict>
	    <key>Listeners</key>
	    <dict>
		<key>SockNodeName</key>
		<string>127.0.0.1</string>
		<key>SockServiceName</key>
		<string>2225</string>
	    </dict>
	</dict>
	<key>inetdCompatibility</key>
	<dict>
	    <key>Wait</key>
	    <false/>
	</dict>
    </dict>
</plist>

After adding the plists above, load and start the launch agents:

launchctl load ~/Library/LaunchAgents/pbcopy.plist
launchctl load ~/Library/LaunchAgents/pbpaste.plist
launchctl start ~/Library/LaunchAgents/pbcopy.plist
launchctl start ~/Library/LaunchAgents/pbpaste.plist

~/.ssh/config

IMPORTANT: use localhost not 127.0.0.1 in RemoteForward directives

Host *
  RemoteForward 2224 localhost:2224
  RemoteForward 2225 localhost:2225

or

Connect with forwarded ports

ssh -R 2225:localhost:2225 <my_linux_host>
ssh -R 2224:localhost:2224 <my_linux_host>

Test it on the remote Linux host

Log into Linux host

Step 1 - Can we get the Mac's clipboard in Linux?

On Mac: Copy some text with ⌘-c

On Linux: Print the Mac's remote clipboard

nc localhost 2225

Step 2 - Can we send to the Mac's clipboard from Linux?

On Linux: Write to Mac's remote clipboard

echo "testing" | nc localhost 2224

On Mac: Paste the text with ⌘-v

Setup emacs to use remote clipboard

On Linux machine

~/.emacs.d/init.el

(defconst *is-a-linux* (eq system-type 'gnu/linux))
(defconst *is-in-ssh* (or (getenv "SSH_CLIENT") (getenv "SSH_TTY") (getenv "SSH_CONNECTION")))
(defconst *is-in-terminal* (not (display-graphic-p))))

(when (and *is-a-linux* *is-in-terminal* *is-in-ssh*)
  (progn
    (defun is-port-listening (host port)
      "Tests if a tcp port is open"
      (interactive)
      (string-match-p "succeeded!"
                      (shell-command-to-string (format "nc -zv %s %d" host port))))

    (defun copy-from-remote ()
      "Use netcat clipboard to paste. PULL"
      (shell-command-to-string "nc localhost 2225"))

    (defun paste-to-remote (text &optional push)
      "Add kill ring entries (TEXT) to netcat clipboard. PUSH."
      (let ((process-connection-type nil))
        (let ((proc (start-process "remote-clipboard-copy" "*Messages*" "nc" "localhost" "2224")))
          (process-send-string proc text)
          (process-send-eof proc))))

    ;; if the reverse forwarding is done we'll use the ports to hook up interprogram cut/paste
    (if (is-port-listening "localhost" 2225)
        (progn
          (message "remote clipboard listening")
          (setq interprogram-cut-function 'paste-to-remote)
          (setq interprogram-paste-function 'copy-from-remote))
      (message "remote clipboard is not listening. skipping..."))))

On Mac

~/.emacs.d/init.el

(defconst *is-a-mac* (eq system-type 'darwin))
(defconst *is-in-terminal* (not (display-graphic-p))))
(defconst *is-in-tmux* (getenv "TMUX"))

(when (and *is-a-mac* *is-in-terminal*)
  (if *is-in-tmux*
      (progn
        "When we are running under macos in tmux.
         Be sure to: brew install reattach-to-user-namespace"
        (defun copy-from-osx ()
          "Use OSX clipboard to paste. PULL"
          (shell-command-to-string "reattach-to-user-namespace pbpaste"))

        (defun paste-to-osx (text &optional push)
          "Add kill ring entries (TEXT) to OSX clipboard. PUSH."
          (let ((process-connection-type nil))
            (let ((proc (start-process "pbcopy" "*Messages*" "reattach-to-user-namespace" "pbcopy")))
              (process-send-string proc text)
              (process-send-eof proc))))

        (setq interprogram-cut-function 'paste-to-osx)
	(setq interprogram-paste-function 'copy-from-osx))

    (progn
      "When we are running in native macos."
      (defun copy-from-osx ()
        "Use OSX clipboard to paste. PULL"
	(shell-command-to-string "pbpaste"))

      (defun paste-to-osx (text &optional push)
        "Add kill ring entries (TEXT) to OSX clipboard. PUSH."
        (let ((process-connection-type nil))
          (let ((proc (start-process "pbcopy" "*Messages*" "pbcopy")))
            (process-send-string proc text)
            (process-send-eof proc))))

      (setq interprogram-cut-function 'paste-to-osx)
      (setq interprogram-paste-function 'copy-from-osx))))

Refs:

NOTE: The clipboard server should also work with Linux using xclip/xsel and/or just reading/writing a local file, e.g /tmp/clipboard.txt.

@jclosure
Copy link
Author

jclosure commented Jul 30, 2019

See here for an explanation of the encoding issues

@jclosure
Copy link
Author

This approach should also work to serve the linux clipboard. Basic example:

ssh with X support to linux box:

ssh -X my_linux_host

run:

export DISPLAY=':0'
ps -ef | xclip
xclip -o | less

something like this to serve xclip.

@jclosure
Copy link
Author

jclosure commented Jul 30, 2019

Playing around with the clipboard for debugging purposes. I recommend LaunchControl for this.

pbcopy.plist
NOTE: that the input is quoted to handle spaces

/bin/bash -c "cat \"${1:-/dev/stdin}\" > /tmp/clipboard.txt; cat /tmp/clipboard.txt | textutil -convert txt -stdin -stdout -encoding 30 | pbcopy"

pbpaste.plist

/bin/sh -c "pbpaste > /tmp/clipboard.txt; cat /tmp/clipboard.txt | textutil -convert txt -stdin -stdout -encoding UTF-8"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment