From 695281a4ce8b9b304b5f3eaa9c2ab16c0860173b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Miri=C4=87?= <1009277+imiric@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:10:46 +0200 Subject: [PATCH] fix: decoding Unicode on X11/Wayland in get-contents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously if `x-select-request-type` was nil, `simpleclip-get-contents` would decode Unicode characters as Latin-1. E.g. copying "🙂" and calling `simpleclip-get-contents` would return: ``` \#("\\U0001f642" 0 10 (foreign-selection nil)) ``` The fix mirrors `gui--selection-value-internal` as mentioned in the previous TODO comment by trying UTF8_STRING, COMPOUND_TEXT, STRING, and `text/plain;charset=utf-8' in order. Full disclosure: this was produced with *heavy* assistance from LLMs, but I reviewed and tested it to the best of my abilities, and it works fine for me on Emacs 30.2 and (X)Wayland. `make test` passes. Fixes #23 --- ert-tests/simpleclip-test.el | 45 ++++++++++++++++++++++++++++++++++++ simpleclip.el | 34 ++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/ert-tests/simpleclip-test.el b/ert-tests/simpleclip-test.el index a0ff8c7..5920a94 100644 --- a/ert-tests/simpleclip-test.el +++ b/ert-tests/simpleclip-test.el @@ -142,6 +142,51 @@ (simpleclip-set-contents kept)))) +;;; Unicode / X selection target fallback + +(defvar simpleclip-test-latin1-emoji + (string 240 159 153 130) + "Raw UTF-8 bytes of U+1F642 as a unibyte string.") + +(defun simpleclip-test--make-selection-reader (selections) + "Return a fake selection reader backed by hash table SELECTIONS." + (lambda (type) + (gethash type selections))) + +(ert-deftest simpleclip-get-contents-utf8-fallback nil + "When `x-select-request-type' is nil, prefer UTF8_STRING over STRING. +This mirrors `gui--selection-value-internal' and avoids interpreting +UTF-8 bytes as Latin-1." + (let ((selections (make-hash-table :test 'equal)) + (x-select-request-type nil)) + (puthash 'UTF8_STRING "🙂" selections) + (puthash 'STRING simpleclip-test-latin1-emoji selections) + (should (equal (simpleclip--get-selection-contents + 'x + (simpleclip-test--make-selection-reader selections)) + "🙂")))) + +(ert-deftest simpleclip-get-contents-utf8-fallback-order nil + "Fall back to STRING when UTF8_STRING is unavailable." + (let ((selections (make-hash-table :test 'equal)) + (x-select-request-type nil)) + (puthash 'STRING simpleclip-test-latin1-emoji selections) + (should (equal (simpleclip--get-selection-contents + 'x + (simpleclip-test--make-selection-reader selections)) + simpleclip-test-latin1-emoji)))) + +(ert-deftest simpleclip-get-contents-explicit-string nil + "When `x-select-request-type' is `STRING', use exactly that target." + (let ((selections (make-hash-table :test 'equal)) + (x-select-request-type 'STRING)) + (puthash 'STRING simpleclip-test-latin1-emoji selections) + (should (equal (simpleclip--get-selection-contents + 'x + (simpleclip-test--make-selection-reader selections)) + simpleclip-test-latin1-emoji)))) + + ;; ;; Emacs ;; diff --git a/simpleclip.el b/simpleclip.el index a26f18f..e5566ec 100644 --- a/simpleclip.el +++ b/simpleclip.el @@ -340,12 +340,15 @@ in GNU Emacs 24.1 or higher." (and (and (featurep 'mac) (fboundp 'x-get-selection)) (x-get-selection 'CLIPBOARD 'NSStringPboardType)) - ;; todo, this should try more than one request type, as in gui--selection-value-internal - (and (fboundp 'gui-get-selection) - (gui-get-selection 'CLIPBOARD (car x-select-request-type))) - ;; todo, this should try more than one request type, as in gui--selection-value-internal - (and (fboundp 'x-get-selection) - (x-get-selection 'CLIPBOARD (car x-select-request-type)))))) + ;; Mirror `gui--selection-value-internal': try each clipboard + ;; data type in order, falling back from UTF-8 to legacy targets. + (simpleclip--get-selection-contents + (window-system) + (lambda (type) + (or (and (fboundp 'gui-get-selection) + (gui-get-selection 'CLIPBOARD type)) + (and (fboundp 'x-get-selection) + (x-get-selection 'CLIPBOARD type)))))))) (t (error "Clipboard support not available"))) (error @@ -370,6 +373,25 @@ in GNU Emacs 24.1 or higher." (error (error "Clipboard support not available")))))) +(defun simpleclip--get-selection-contents (win-system get-selection) + "Get clipboard contents using selection target fallback. +WIN-SYSTEM is the current window-system symbol. GET-SELECTION is +a function (TYPE) that returns the selection for that data type. +Mirrors `gui--selection-value-internal'." + (when (memq win-system '(x pgtk haiku)) + (let ((request-type (or x-select-request-type + '(UTF8_STRING COMPOUND_TEXT STRING text/plain\;charset=utf-8))) + text) + (with-demoted-errors "simpleclip--get-selection-contents: %S" + (if (consp request-type) + (while (and request-type (not text)) + (setq text (funcall get-selection (car request-type))) + (setq request-type (cdr request-type))) + (setq text (funcall get-selection request-type)))) + (when text + (remove-text-properties 0 (length text) '(foreign-selection nil) text)) + text))) + ;;;###autoload (defun simpleclip-set-contents (str-val) "Set the contents of the system clipboard to STR-VAL."