Subject: [PATCH 1/3] Ignore OSC sequences in term instead of printing them
From: Stephane Zermatten <[email-masked]>
To: [email-masked]
Date: Mon, 05 May 2025 17:30:35 +0300
List-Id: <debbugs-submit.debbugs.gnu.org>

Tags: patch

This change detects OSC sequences and ignores them. This is what's
normally expected and avoids strange outputs.

Starting with version 4, the fish shell has started sending out OSC
sequences by default, which looks pretty bad on term. This change avoids
this particular problem.

This is PATCH 1/3, because I'd like to propose making it possible to
handle OSC sequences in term in follow-up changes. Emacs already has
handlers for OSC sequences in ansi-osc, why not make use of them?

In GNU Emacs 31.0.50 (build 5, x86_64-apple-darwin23.6.0, NS
 appkit-2487.70 Version 14.7.5 (Build 23H527)) of 2025-05-02 built on
 boomer.zia
Repository revision: 99ca41b6ef300653a0d15b73a0c0d446a9a9e059
Repository branch: master
Windowing system distributor 'Apple', version 10.3.2487
System Description:  macOS 14.7.5

Configured using:
 'configure --program-suffix=-head --with-tree-sitter
 --with-native-compilation=aot'

From 818736960d03ea8be254f81e5267e54c9ad27806 Mon Sep 17 00:00:00 2001
From: Stephane Zermatten <[email-masked]>
Date: Sat, 3 May 2025 00:03:12 +0300
Subject: [PATCH 1/3] Ignore OSC sequences in term instead of printing them

Terminals are expected to just skip unknown OSC sequences. Doing this
avoids strange output, for example when fish4 is run inside of term.

* lisp/term.el (term-emulate-terminal) skip OSC sequences
---
 lisp/term.el            | 17 +++++++++++++++++
 test/lisp/term-tests.el | 31 +++++++++++++++++++++++++++++++
 2 files changed, 48 insertions(+)

diff --git a/lisp/term.el b/lisp/term.el
index a971300c055..fa609058d77 100644
--- a/lisp/term.el
+++ b/lisp/term.el
@@ -3030,6 +3030,10 @@ term-control-seq-regexp
    "\e\\(?:[DM78c]\\|"
    ;; another Emacs specific control sequence,
    "AnSiT[^\n]+\n\\|"
+   ;; OSC (see [ECMA-48] section 8.3.89 "Operation System Command") In
+   ;; addition to the range defined there, this regexp allows non-ascii
+   ;; (UTF-8) characters.
+   "\\][^\x00-\x07\x0e-\x1f\x7f]*\\(?:\a\\|\e\\\\\\)?\\|"
    ;; or an escape sequence (section 5.4 "Control Sequences"),
    "\\[\\([\x30-\x3F]*\\)[\x20-\x2F]*[\x40-\x7E]\\)\\)")
   "Regexp matching control sequences handled by term.el.")
@@ -3232,6 +3236,19 @@ term-emulate-terminal
                             (split-string ctl-params ";"))
                            (aref str (1- ctl-end))
                            private))))
+                     (?\] ;; An OSC sequence that's just skipped for now
+                      (when (and
+                             (>= ctl-end str-length)
+                             (not
+                              (or (eq ?\a (aref str (1- ctl-end)))
+                                  (string=
+                                   "\e\\"
+                                   (substring
+                                    str (max i (- ctl-end 2)) ctl-end)))))
+                        ;; Continue ignoring until the end marker.
+                        (setq term-terminal-undecoded-bytes "\e]"))
+                      ;; Consume everything
+                      (setq i ctl-end))
                      (?D ;; Scroll forward (apparently not documented in
                       ;; [ECMA-48], [ctlseqs] mentions it as C1
                       ;; character "Index" though).
diff --git a/test/lisp/term-tests.el b/test/lisp/term-tests.el
index ffb341f3b52..f70245c6af3 100644
--- a/test/lisp/term-tests.el
+++ b/test/lisp/term-tests.el
@@ -419,6 +419,37 @@ term-undecodable-input
                    (term-test-screen-from-input
                     40 1 bytes)))))
 
+(ert-deftest term-ignore-osc()
+  ;; BEL-terminated OSC sequence
+  (should (equal "test"
+                 (term-test-screen-from-input
+                  40 1 "te\e]0;window title\ast")))
+  ;; ESC \-terminated OSC sequence
+  (should (equal "test"
+                 (term-test-screen-from-input
+                  40 1 "te\e]0;window title\e\\st")))
+  ;; Long OSC sequence split into multiple chunks
+  (should (equal "test"
+                 (term-test-screen-from-input
+                  40 1 '("te\e]0;win" "dow " " title\ast"))))
+  ;; OSC sequence that start and ends with the chunk
+  (should (equal "test"
+                 (term-test-screen-from-input
+                  40 1 '("te" "\e]0;window " "title\a" "st"))))
+
+  ;; Invalid control characters break out of the OSC sequence, for
+  ;; safety.
+  (should (equal "tetitlest"
+                 (term-test-screen-from-input
+                  40 1 '("te\e]0;window\x05title\ast"))))
+
+  (let ((locale-coding-system 'utf-8-unix))
+    ;; An OSC sequence with multibyte UTF-8 characters. Not exactly
+    ;; standard-compliant, but too common not to support.
+    (should (equal "test"
+                   (term-test-screen-from-input
+                    40 1 "te\e]0;\xce\xb1\xce\xb2\e\\st")))))
+
 (provide 'term-tests)
 
 ;;; term-tests.el ends here
-- 
2.47.0