I’ve recently been using Emacs in a terminal instead of its GTK frontend. This is mostly because the PGTK GUI for the version of Emacs (version 30.2… so the newest AFAIK) shipped in Fedora is abysmally slow when full-screened at 4K display resolution. This doesn’t seem to be the case with GNOME’s terminal (full-screened at the same resolution), so now I’m using Emacs in the terminal as a workaround. My workflow hasn’t really changed in any meaningful way after the switch. The only surprise was that copying and pasting was not integrated into the system clipboard anymore. So anything I put in Emacs’ kill ring was just stuck there and only usable in Emacs itself. I couldn’t then paste it out to another program like Chromium, for example. In hindsight, of course Emacs isn’t integrated into the clipboard anymore! Why would I think otherwise? Vim doesn’t work this way… or any other terminal program for that matter. I should be using the terminal’s built-in copy and paste functionality (Ctrl-Shift-C and Ctrl-Shift-V), but that gets hairy when you have split windows in Emacs and you only want to copy what’s in your immediate window (If you aren’t familiar with Emacs terminology, windows probably don’t mean what you think they mean. It’s an old program, cut it some slack!)

So lets fix this! I’m running GNOME on Wayland, so there’s some terminal programs that could be useful: wl-copy and wl-paste. Check out their man pages for more information, but very basically, wl-copy takes whatever is in stdin and puts it in Wayland’s clipboard and wl-paste echoes whatever is in the clipboard to stdout. Here’s an example of using them in the terminal:

nathan@nathans-laptop:~$ echo "Hello, World!" | wl-copy
nathan@nathans-laptop:~$ wl-paste
Hello, World!

So lets use these programs to integrate Emacs’ kill ring with the system clipboard! We can write some elisp functions to call external processes and set those as the interprogram-paste-function and interprogram-cut-function. These are builtin variables denoting a function to call whenever something is put in the kill ring (cut) or yanked (paste).

(when (and (executable-find "wl-paste") (executable-find "wl-copy"))
  (defun wl-copy (text)
    "Send text to the Wayland clipboard."
    (let ((wl-copy-process (make-process :name "wl-copy"
					 :command '("wl-copy")
					 :connection-type 'pipe)))
      (process-send-string wl-copy-process text)
      (process-send-eof wl-copy-process)))

  (defun wl-paste ()
    "Return the contents of the Wayland clipboard as a string, or nil."
    (let ((wl-paste-output (with-temp-buffer
			     (call-process "wl-paste" nil t nil)
			     (buffer-string))))
      (when (and wl-paste-output (not (string= wl-paste-output "")))
        ;; Remove a single trailing newline that wl-paste adds
        (replace-regexp-in-string "\n\\'" "" wl-paste-output))))
  
  (setq interprogram-paste-function 'wl-paste)
  (setq interprogram-cut-function 'wl-copy)
  (setq select-enable-clipboard t))

For wl-copy notice that we are creating a process and then sending the text to it via stdin using process-send-string. Pasting is a bit more interesting. We first create a temporary buffer and call the wl-paste process and configuring it to write stdout and stderr to that buffer by setting the DESTINATION argument to t. We can then copy the text in the temporary buffer to our wl-paste-output variable by using the buffer-string function. The rest is just boring processing to meet the expected contract of interprogram-paste-function.

And there we go! Copying and pasting works just like it did in the GUI version of Emacs.

Leave a Reply

Discover more from Nathaniel Wright

Subscribe now to keep reading and get access to the full archive.

Continue reading