2021-11-06 00:06:55 +00:00
#+TITLE : Pasting the Org Clipboard
#+AUTHOR : Howard X. Abrams
#+DATE : 2020-09-15
2022-06-27 21:17:28 +00:00
A literate programming file of functions for formatting Emacs text and code into and out to the system clipboard.
2021-11-06 00:06:55 +00:00
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :exports none
2022-03-09 18:45:37 +00:00
;;; org-clipboard --- Functions for formatting the clipboard. -*- lexical-binding: t; -* -
;;
2023-02-23 17:35:36 +00:00
;; © 2020-2023 Howard X. Abrams
2022-06-18 00:25:47 +00:00
;; Licensed under a Creative Commons Attribution 4.0 International License.
2022-03-09 18:45:37 +00:00
;; See http://creativecommons.org/licenses/by/4.0/
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams >
;; Maintainer: Howard X. Abrams
;; Created: September 15, 2020
;;
;; This file is not part of GNU Emacs.
;;
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
;; ~/other/hamacs/org-clipboard.org
;; And tangle the file to recreate this one.
;;
;;; Code:
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-06 00:06:55 +00:00
* Introduction
2022-06-27 21:17:28 +00:00
Sure, I try to keep my text editing world /inside/ the internal consistency and mouse-less world of Emacs. But life is more complicated, especially on my Mac system given to me for work. The code shown before allows me to interact with other applications, including:
- Copying code from Emacs to the clipboard that I can paste into apps like Slack
- Copying Org notes to the clipboard so that when I paste them into web apps and Slack, they maintain their formatting
- Copying formatted text from web pages and Slack and pasting them in Org files /as org-formatted text/ .
* Into the Clipboard
[[http://mbork.pl/2022-06-20_Copying_the_current_location ][This essay ]] from =mbork= has an interesting idea of being able to select code, and copy it to the clipboard with /extra information/ about the location. The location is the filename (relative to the project), as well as the project name and line number, but since every buffer may not have all this information, we’ ll make some best guesses:
#+begin_src emacs-lisp
2022-08-09 16:49:27 +00:00
(defun current-location (start-line)
2022-06-27 21:17:28 +00:00
"Show the current location and put it into the kill ring.
Use the filename relative to the current projectile root directory.
If called non-interactively, return the location as a string."
2022-08-09 16:49:27 +00:00
(interactive "P")
2022-06-27 21:17:28 +00:00
(let* ((project-name (projectile-project-name))
(file-name (when (and buffer-file-name (projectile-project-root))
(file-relative-name buffer-file-name (projectile-project-root))))
2022-08-09 16:49:27 +00:00
(line-number (if (and (called-interactively-p) start-line)
(line-number-at-pos nil t)
start-line))
2021-11-06 00:06:55 +00:00
2022-06-27 21:17:28 +00:00
(location (cond
2022-08-09 16:49:27 +00:00
((and project-name file-name line-number)
2022-06-27 21:17:28 +00:00
(format "%s :: %s : %s" project-name file-name line-number))
2022-08-09 16:49:27 +00:00
((and project-name file-name)
(format "%s :: %s" project-name file-name))
((and file-name line-number)
2022-06-27 21:17:28 +00:00
(format "%s : %d" file-name line-number))
2022-08-09 16:49:27 +00:00
(file-name file-name)
2022-06-27 21:17:28 +00:00
(project-name
(format "project: %s" project-name))
(t ""))))
(if (not (called-interactively-p))
location
(kill-new location)
(message location))))
#+end_src
Use the =current-location= function, along with the /region/ or /function/ and copy it into the clipboard:
#+begin_src emacs-lisp
(defun code-region-to-clipboard (start end)
"Copy the active region along with a location header to kill ring.
Calls `current-location' to get the header."
(interactive "r")
2022-08-09 16:49:27 +00:00
(let ((location (current-location start)))
(kill-new
(format "From %s …\n%s" location (buffer-substring-no-properties start end)))
(message "Copied code to clipboard, %s" location)))
2022-06-27 21:17:28 +00:00
#+end_src
And if there is no active region, let’ s attempt to copy the /function/ (whatever that may mean), that we get with the [[help:which-function-mode ][which-function-mode ]] (see [[https://www.emacswiki.org/emacs/WhichFuncMode ][the Emacs wiki ]] for details):
#+begin_src emacs-lisp
(defun code-function-to-clipboard ()
"Copy the current function along with a location header to kill ring.
Calls `current-location' to get the header, and uses the `which-function'
for the name of the function."
(interactive)
(let ((func-name (which-function))
(func-start (save-excursion (beginning-of-defun) (point)))
(func-end (save-excursion (end-of-defun) (point))))
(kill-new
(format "%s (%s)…\n%s"
(current-location func-start) func-name (buffer-substring-no-properties func-start func-end)))
(message "Copied `%s' to clipboard" func-name)))
#+end_src
2021-11-06 00:06:55 +00:00
2022-06-27 21:17:28 +00:00
Code is easy to paste, but what about org, as we could formatted it to HTML first, before putting it on the clipboard. For this, I’ m going to use the illustrious [[https://pandoc.org/ ][pandoc project ]] to convert my org file to either Markdown (for Slack) or rich-text (for highly formatted text for the MacOS clipboard). This function is MacOS specific, since it uses [[https://www.unix.com/man-page/osx/1/pbcopy/ ][pbcopy ]]. I will convert it to also use [[https://linux.die.net/man/1/xclip ][xclip ]].
#+begin_src emacs-lisp
(defun org-to-clipboard (rich-text-p)
"Copy the region or Org subtree to the clipboard as Rich Text."
(interactive "cDo you want Rich Text conversion? [y/n] ")
(save-excursion
(unless (region-active-p) (org-mark-subtree))
(let* ((contents
(buffer-substring-no-properties (region-beginning) (region-end)))
(tmp-file (make-temp-file "ha-clipboard-"))
(text-cmd (format "pandoc --from org --to markdown --standalone %s | pbcopy" tmp-file))
(rich-cmd (format "pandoc --from org --to rtf --standalone %s | pbcopy -Prefer rtf" tmp-file)))
(with-temp-file tmp-file
(insert contents))
(cond
((or (eq rich-text-p ?y) (eq rich-text-p 13)) (shell-command rich-cmd))
(t (shell-command text-cmd))))))
#+end_src
2021-11-06 00:06:55 +00:00
2022-06-27 21:17:28 +00:00
And a do-what-I-mean function to call these guys based on context:
#+begin_src emacs-lisp
(defun code-to-clipboard-dwim ()
"Puts the region or function on the kill-ring along with location details.
The location details includes the project and file name along with line number."
(interactive)
(cond
((eq major-mode 'org-mode) (call-interactively 'org-to-clipboard))
((region-active-p) (call-interactively 'code-region-to-clipboard))
(t (code-function-to-clipboard))))
#+end_src
And we need a keybinding:
#+begin_src emacs-lisp
(ha-leader
"y" '("copy location" . current-location)
"Y" '("copy code" . code-to-clipboard-dwim))
#+end_src
* From The Clipboard
Copying regular text into buffers if well supported in Emacs, but this section describes functions to help convert content from the operating system's clipboard into org-mode-compatible text.
Each operating system as a different way of working with the clipboard, so let's create an operating-system abstraction:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-09 00:02:39 +00:00
(defun ha-get-clipboard ()
2021-11-06 00:06:55 +00:00
"Returns a list where the first entry is the content type,
either :html or :text, and the second is the clipboard contents."
2021-12-13 18:45:32 +00:00
(if (ha-running-on-macos?)
2021-11-09 00:02:39 +00:00
(ha-get-mac-clipboard)
(ha-get-linux-clipboard)))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-06 00:06:55 +00:00
Let's define the clipboard for a Mac. The challenge here is that we need to binary unpack the data from a call to Applescript.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-03-03 23:10:16 +00:00
(defun ha-get-mac-clipboard ()
"Returns a list where the first entry is the content type,
either :html or :text, and the second is the clipboard contents."
(cl-destructuring-bind (exit-code contents)
(shell-command-with-exit-code "osascript" "-e" "the clipboard as \"HTML\"")
(if (= 0 exit-code)
(list :html (ha-convert-applescript-to-html contents))
(list :text (shell-command-to-string "osascript -e 'the clipboard'")))))
2021-11-06 00:06:55 +00:00
2022-03-03 23:10:16 +00:00
(defun ha-convert-applescript-to-html (packed-contents)
"Applescript's clipboard returns the contents in a packed array.
Convert and return this encoding into a UTF-8 string."
(cl-flet ((hex-pack-bytes (tuple) (string-to-number (apply 'string tuple) 16)))
(let* ((data (-> packed-contents
(substring 10 -2) ; strips off the =«data RTF= and =»\= bits
(string-to-list)))
(byte-seq (->> data
(-partition 2) ; group each two hex characters into tuple
(mapcar #'hex-pack-bytes))))
(decode-coding-string
(mapconcat #'byte-to-string byte-seq "") 'utf-8))))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-06 00:06:55 +00:00
2022-10-18 03:49:00 +00:00
And define the same interface for Linux. Keep in mind, we need the exit code from calling a process, so I am going to define/use a helper function.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-09 00:02:39 +00:00
(defun ha-get-linux-clipboard ()
"Return the clipbaard for a Unix-based system. See `ha-get-clipboard'."
2022-03-03 23:10:16 +00:00
(cl-destructuring-bind (exit-code contents)
2021-11-06 00:06:55 +00:00
(shell-command-with-exit-code "xclip" "-o" "-t" "text/html")
(if (= 0 exit-code)
(list :html contents)
(list :text (shell-command-to-string "xclip -o")))))
(defun shell-command-with-exit-code (program &rest args)
"Run PROGRAM with ARGS and return the exit code and output in a list."
(with-temp-buffer
(list (apply 'call-process program nil (current-buffer) nil args)
(buffer-string))))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-06 00:06:55 +00:00
2022-06-27 21:17:28 +00:00
** Converting to Org
2021-11-06 00:06:55 +00:00
Let's work top-down at this point with the interactive function that inserts the clipboard into the current buffer:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-09 00:02:39 +00:00
(defun ha-org-yank-clipboard ()
2021-11-06 00:06:55 +00:00
"Yanks (pastes) the contents of the Apple Mac clipboard in an
org-mode-compatible format."
(interactive)
2021-11-09 00:02:39 +00:00
(insert (ha-org-clipboard)))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-06 00:06:55 +00:00
2022-06-27 21:17:28 +00:00
This function does the heavy lifting. Note that I will need another function to tidy up the output from =pandoc= that will be more to my liking.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-06-27 21:17:28 +00:00
(defun ha-org-clipboard ()
"Return the contents of the clipboard in org-mode format."
(seq-let (type contents) (ha-get-clipboard)
(with-temp-buffer
(insert contents)
(if (eq :html type)
(shell-command-on-region (point-min) (point-max)
"pandoc -f html -t org --wrap=none --ascii" t t)
(shell-command-on-region (point-min) (point-max)
"pandoc -f markdown -t org --wrap=none --ascii" t t))
(ha-html-paste-touchup)
(buffer-substring-no-properties (point-min) (point-max)))))
(defun ha-html-paste-touchup ()
"Attempts to fix the org produced by `pandoc''.
Much of this is also spurious characters from Slack.
Note that this isn't perfect, but a good beginning."
(interactive)
2022-08-09 16:49:27 +00:00
(dolist (combo `((" " " ") ; Pandoc's fixed space needs to go
2022-06-27 21:17:28 +00:00
;; Convert links to a user to an item element:
(,(rx "[[https://app.slack.com/team" (one-or-more (not "]")) "]["
(group (one-or-more (not "]"))) "]]")
" - *\\1* : ")
;; Make the link to the original more obvious:
(,(rx "[[" (group "https://" (one-or-more any) ".slack.com/archives/ "
(one-or-more (not "]"))) "]["
(group (one-or-more (not "]"))) "]]")
" [[\\1 ][(Link to original message) ]]")
(,(rx "[[https://slack-imgs.com" (one-or-more (not "]"))
"][" (one-or-more (not "]")) "]]")
" - *\\1* : ")
(,(rx "[[data:" (one-or-more (not "]"))
(optional "][" (one-or-more (not "]"))) "]]") "")
(,(rx "[[https://slack-imgs.com" (one-or-more (not "]"))
(optional "][" (one-or-more (not "]"))) "]]") "")
(,(rx "\\" line-end) "") ; Doing this twice covers both
(,(rx "\\" line-end) "") ; single and double backslashes
(,(rx "(edited)") "")
(,(rx line-start "Last reply" (one-or-more any)) "")
(,(rx line-start "New" line-end) "")
(,(rx line-start "//" line-end) "") ; Odd choice of a separator
(,(rx line-start "<<" (one-or-more any) " >>" line-end) "")
;; Shrink multiple blank lines into a single one:
(,(rx line-start
(zero-or-more space) (regex "\n")
(zero-or-more space) (regex "\n")) "")))
(seq-let (search replace) combo
(goto-char (point-min))
(while (re-search-forward search nil t)
(replace-match replace)))))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-06 00:06:55 +00:00
2022-06-27 21:17:28 +00:00
Bind these functions to the /local/ mode key sequence:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-24 00:34:48 +00:00
(with-eval-after-load 'ha-org
2023-05-01 18:49:33 +00:00
(ha-local-leader :keymaps 'org-mode-map
"y" 'ha-org-yank-clipboard))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-06 00:06:55 +00:00
* Technical Artifacts :noexport:
Let's provide a name so we can =require= this file:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-06 00:06:55 +00:00
(provide 'ha-org-clipboard)
;;; ha-org-clipboard.el ends here
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-06 00:06:55 +00:00
#+DESCRIPTION : A literate programming version of functions for formatting the clipboard.
#+PROPERTY : header-args:sh :tangle no
2023-07-05 20:20:55 +00:00
#+PROPERTY : header-args:emacs-lisp :tangle yes
2021-11-06 00:06:55 +00:00
#+PROPERTY : header-args :results none :eval no-export :comments no mkdirp yes
#+OPTIONS : num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
#+OPTIONS : skip:nil author:nil email:nil creator:nil timestamp:nil
#+INFOJS_OPT : view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js