5

I'm running this emacs

GNU Emacs 25.2.2 (x86_64-pc-linux-gnu) of 2017-09-22, modified by Debian

directly in a Gnome terminal by calling emacs, which is actually a link to /etc/alternatives/emacs. This is on an Ubuntu 18.04 Linux.

Emacs copy function, C-c in cua mode, doesn't copy into the clipboard. I followed this advice and installed this package

M-x package-install RET xclip RET

and it still didn't work. Now I have added a simple function to my .emacs file :

(defun copy-region-to-clipboard ()
  "Copies region to X-clipboard invoking `xclip' in shell"
  (interactive)
  (shell-command-on-region
   (region-beginning)
   (region-end)
   "xclip -i -selection clipboard"))

(global-set-key (kbd "<f1>") 'copy-region-to-clipboard)

which does work. But I have to press F1 and Emacs won't return control of the cursor until cancelling with C-g. After cancelling with C-g, the text disappears from the clipboard.

So the order of operation is

Select-region 
    -> F1 
        ->  Move focus to other window and paste 
            -> Move focus back to Emacs and C-g before continuing

I wonder if there is a tweak to this program that will allow xclip to fork asynchronously with necessary input data and return control to Emacs? And then somehow chain this new function async-copy-region-to-clipboard onto the end of the C-c functionality?


  • Edit: Adding & to end of command: xclip -i -selection clipboard &, does not change behavior - Emacs still waits.

  • Edit With the help of the excellent selected answer I tried using xsel instead of xclip and found that xsel doesn't block like xclip does. This is long known behavior of xclip. Also discussed here.

xclip could also be used for this purpose. Unlike xsel, it works better when printing a raw bitstream that does not fit the current locale. Nevertheless, it is neater to use xsel because xclip does not close STDOUT after it has read from the tmux buffer. As such, tmux does not know that the copy task has completed, and continues to wait for xclip to terminate, thereby rendering tmux unresponsive. A workaround is to redirect {{ic|STDOUT} to /dev/null:

However, I found that neither of these worked for the Emacs solution, and Emacs still hung:

xclip -i -selection clipboard > /dev/null
xclip -i -selection clipboard > /dev/null &

My final working code is:

;; .emacs
...
(defun copy-region-to-clipboard ()
  "Copies region to X-clipboard invoking `xsel' in shell"
  (interactive)
  (if (and(use-region-p) (not (display-graphic-p)))
      (shell-command-on-region
       (region-beginning)
       (region-end)
       "xsel -i -b"
       )))

(global-set-key (kbd "<f1>") 'copy-region-to-clipboard)

(defun workaround-copy-region-as-kill ()
  "Run `some-command' and `some-other-command' in sequence."
  (interactive)
  (when (and(use-region-p) (not (display-graphic-p)))
    (copy-region-to-clipboard)
    (copy-region-as-kill 0 0 t)))

(defun workaround-kill-region  ()
  "Run `some-command' and `some-other-command' in sequence."
  (interactive)
  (when (and(use-region-p) (not (display-graphic-p)))
    (copy-region-to-clipboard)
    (kill-region 0 0 t)))

;; Note: 'workaround-copy-region-as-kill' is REPLACING 'copy-region-as-kill'
;; in cua--cua-keys-keymap
;; so we call 'copy-region-as-kill' from the end of it. 
(define-key cua--cua-keys-keymap (kbd "C-c <timeout>") 'workaround-copy-region-as-kill)
(define-key cua--cua-keys-keymap (kbd "C-k <timeout>") 'workaround-kill-region)

;;(global-set-key (kbd "C-c") 'workaround-copy-region-as-kill) FAIL!

The technique of mapping C-c <timeout> was provided by this answer to my follow-on question.

Craig Hicks
  • 304
  • 2
  • 9
  • Why do you use the terminal version of emacs? – Tobias Mar 07 '19 at 10:29
  • It starts up and it exits instantly. Overall, I'm using less and faster keystrokes going between the terminal emacs (`C-x C-z`, `fg`) than I was using terminal mode inside emacs - and experiencing less emacs buffer bloat. ----- Also, the same emacs usage pattern carries over to working on a remote through an ssh connection inside a terminal. I was previously using tramp for that, but there were problems with that: time, switching between sudo and non-sudo mode, confusion between local and remote file paths. – Craig Hicks Mar 07 '19 at 17:33
  • @Tobias Although it's true clipping on (windowless) remote isn't going to work with the method described above. Instead mouse drag would be required. This is a point of concession to your viewpoint. – Craig Hicks Mar 07 '19 at 17:48

1 Answers1

5

I'm also a loyal emacs-nw user, and have struggled with this over the recent emacs versions. What works for me, and without any need to C-g, follows. Note that it has a safety check that is linux-specific, so in a linux environment, if there is no X-display associated, it will try to operate on the lowest-numbered available X-display instead of silently crashing. Also, it uses the linux program xsel instead of xclip:

  (defun my-copy-to-xclipboard(arg)
    (interactive "P")
    (cond
      ((not (use-region-p))
       (error "Nothing to yank to X-clipboard"))
      ((not (display-graphic-p))
       (let ((x-display
               (or (getenv "DISPLAY")
                   (when (getenv "TMUX")
                     (shell-command-to-string
                       "tmux_display=$(ps e $(pgrep -f \"tmux attach\") |\
                        grep -o \"DISPLAY=[^ ]*\") && printf ${tmux_display##*=}"))
                   (shell-command-to-string
                 "cd /tmp/.X11-unix && for x in X*; do printf \":${x#X}\"; break; done")))
             exit-code)
         (if (not (string-match ":" x-display))
           (error "No X-display found.")
          (cond
           ((zerop (setq exit-code
                     (shell-command-on-region (region-beginning) (region-end)
                       (format "xsel --display %s -i -b" x-display)))))
           ((= 127 exit-code)
            (error "Is program `xsel' installed?"))
           (*
            (error "xsel exited with code %s" exit-code))))))
      ((display-graphic-p)
         (condition-case err
           (progn
             (call-interactively 'clipboard-kill-ring-save))
           (error "Clipboard-failure."))))
    (message "Region yanked to X-clipboard")
    (when arg
      (kill-region  (region-beginning) (region-end)))
    (deactivate-mark))

  (defun my-cut-to-xclipboard()
    (interactive)
    (my-copy-to-xclipboard t))

  (defun my-paste-from-xclipboard()
    "Uses shell command `xsel -o' to paste from x-clipboard. With
  one prefix arg, pastes from X-PRIMARY, and with two prefix args,
  pastes from X-SECONDARY."
    (interactive)
    (if (display-graphic-p)
      (clipboard-yank)
     (let*
       ((opt (prefix-numeric-value current-prefix-arg))
        (opt (cond
         ((=  1 opt) "b")
         ((=  4 opt) "p")
         ((= 16 opt) "s"))))
      (insert (shell-command-to-string (concat "xsel -o -" opt))))))

  (global-set-key (kbd "C-c C-w") 'my-cut-to-xclipboard)
  (global-set-key (kbd "C-c M-w") 'my-copy-to-xclipboard)
  (global-set-key (kbd "C-c C-y") 'my-paste-from-xclipboard)

EDIT #1: Minor change to cond clause. Changed t (when (display-graphic-p)..) to just((display-graphic-p)`

EDIT #2: Better checking of $DISPLAY, including handling cases of running emacsclient within tmux.

EDIT #3: Improve checking of $DISPLAY to handle ssh cases (thanks: comment from @ishmael)

user1404316
  • 769
  • 5
  • 12
  • Very helpful, thanks. Using `xsel` turned out to be the key, and there is a story behind that. – Craig Hicks Mar 07 '19 at 21:29
  • I am now looking at the `/tmp` directory of a remote `Ubuntu 16.04` sever not running X, and there is nothing for `/tmp/X*`. This part of your answer I don't understand. – Craig Hicks Mar 07 '19 at 21:36
  • @CraigHicks: That's the point. Those `/tmp` files should exist If and only if an X-server is running. Thus, if your emacs is `-nw`, ie. `(not display-graphic-p), but there is an X-server, and thus an X-clipboard, find the x-display id and send the data to that display's x-clipboard. This may mess up in cases of multiple users accounts operating multiple workstations ("seats" in X11 terminology) on the host machine; maybe it needs to additionally test for matching UID's for the x-display – user1404316 Mar 07 '19 at 21:56
  • Interestingly, on my desktop Ubuntu 18.04 linux system, there is also nothing corresponding to `/tmp/X*`. I see processes: `/usr/bin/Xwayland` and `/usr/lib/xorg/Xorg` running. – Craig Hicks Mar 07 '19 at 22:08
  • @CraigHicks: Don't know what to tell you. That would be a separate question for someone else to answer. – user1404316 Mar 07 '19 at 22:27
  • The situation is that my system Ubuntu 18.04 is operating with a Gnome-on-Wayland window system. Part of that system includes a psuedo-X interface, necessary for back compatibility. That psuedo-X interface has a single seat `:0`, which is guaranteed to be always present. Therefore the code will work on Ubuntu 18.04 with Wayland as is - but as your correctly state it is not safe for use with honest X systems. – Craig Hicks Mar 26 '19 at 09:08
  • This is an excellent solution to a very old problem! One small suggestion: I would change `(if (not (string= (substring x-display 0 1) ":"))` to `(if (not (cl-search ":" x-display))` because when you SSH, the DISPLAY variable contains more than just a two numbers separated by a colon. Instead, it's something like "localhost:46.0". This way, the function will work across an SSH connection, as long as you enabled X-forwarding using "ssh -X" or "ssh -Y". – ishmael Oct 18 '19 at 00:10
  • @ishmael: Thanks. How about `(if (not (string-match ":" x-display))` to avoid the cl library? – user1404316 Oct 23 '19 at 10:07
  • @user1404316 Even better, thanks! – ishmael Oct 24 '19 at 01:15