From 630a13ac4631866889bab1177e06ca1d693708c1 Mon Sep 17 00:00:00 2001 From: Augusto Stoffel Date: Wed, 25 Aug 2021 12:29:22 +0200 Subject: [PATCH] Add support for OSC escape codes in comint * doc/emacs/misc.texi (Shell Mode): Document it. * lisp/comint.el (comint-osc-handlers, comint-osc--marker): New variables. (comint-osc-process-output): New function. (comint-osc-hyperlink-map): New map. (comint-osc-hyperlink-handler): New function. --- doc/emacs/misc.texi | 13 +++++++ etc/NEWS | 19 ++++++++-- lisp/comint.el | 85 ++++++++++++++++++++++++++++++++++++++++++ lisp/net/browse-url.el | 1 + 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/doc/emacs/misc.texi b/doc/emacs/misc.texi index 528cfa94c66..47e3e11d339 100644 --- a/doc/emacs/misc.texi +++ b/doc/emacs/misc.texi @@ -1113,6 +1113,19 @@ subshell: @end example @end table +By default, Shell mode handles common @acronym{ANSI} escape codes (for +instance, for changing the color of text). Emacs also optionally +supports some extend escape codes, like some of the @acronym{OSC} +(Operating System Codes) if you put the following in your init file: + +@lisp +(add-hook 'comint-output-filter-functions 'comint-osc-process-output) +@end lisp + +With this enabled, the output from, for instance, @code{ls +--hyperlink} will be made into clickable buttons in the Shell mode +buffer. + @cindex Comint mode @cindex mode, Comint Shell mode is a derivative of Comint mode, a general-purpose mode for diff --git a/etc/NEWS b/etc/NEWS index 07a78216b88..bfc1d3ef2e2 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -1561,10 +1561,6 @@ If non-nil, 'shell-mode' handles implicit "cd" commands, changing the directory if the command is a directory. Useful for shells like "zsh" that has this feature. -+++ -*** 'comint-delete-output' can now save deleted text in the kill-ring. -Interactively, 'C-u C-c C-o' triggers this new optional behavior. - ** Eshell --- @@ -3026,6 +3022,21 @@ default are unaffected.) states to be maintained if 'so-long-mode' replaces the original major mode. By default, these new options support 'view-mode'. +** Comint + ++++ +*** Support for OSC escape sequences. +Adding the new 'comint-osc-process-output' to +'comint-output-filter-functions' enables the interpretation of OSC +("Operating System Command") escape sequences in comint buffers. By +default, only OSC 8, for hyperlinks, is acted upon. Adding more +entries to `comint-osc-handlers' allows a customized treatment of +further escape sequences. + ++++ +*** 'comint-delete-output' can now save deleted text in the kill-ring. +Interactively, 'C-u C-c C-o' triggers this new optional behavior. + * New Modes and Packages in Emacs 28.1 diff --git a/lisp/comint.el b/lisp/comint.el index 7af8e8fd2a5..b2557dd5029 100644 --- a/lisp/comint.el +++ b/lisp/comint.el @@ -3887,6 +3887,91 @@ REGEXP-GROUP is the regular expression group in REGEXP to use." ;; don't advance, so ensure forward progress. (forward-line 1))) (nreverse results)))) + + +;;; OSC escape sequences (Operating System Commands) +;;============================================================================ +;; Adding `comint-osc-process-output' to `comint-output-filter-functions' +;; enables the interpretation of OSC escape sequences. By default, only +;; OSC 8, for hyperlinks, is acted upon. Adding more entries to +;; `comint-osc-handlers' allows a customized treatment of further sequences. + +(defvar-local comint-osc-handlers '(("8" . comint-osc-hyperlink-handler)) + "Alist of handlers for OSC escape sequences. +See `comint-osc-process-output' for details.") + +(defvar-local comint-osc--marker nil) + +(defun comint-osc-process-output (_) + "Interpret OSC escape sequences in comint output. +This function is intended to be added to +`comint-output-filter-functions' in order to interpret escape +sequences of the forms + + ESC ] command ; text BEL + ESC ] command ; text ESC \\ + +Specifically, every occurrence of such escape sequences is +removed from the buffer. Then, if `command' is a key of the +`comint-osc-handlers' alist, the corresponding value, which +should be a function, is called with `command' and `text' as +arguments, with point where the escape sequence was located." + (let ((bound (process-mark (get-buffer-process (current-buffer))))) + (save-excursion + (goto-char (or comint-osc--marker + (and (markerp comint-last-output-start) + (eq (marker-buffer comint-last-output-start) + (current-buffer)) + comint-last-output-start) + (point-min))) + (when (eq (char-before) ?\e) (backward-char)) + (while (re-search-forward "\e]" bound t) + (let ((pos0 (match-beginning 0)) + (code (and (re-search-forward "\\=\\([0-9A-Za-z]*\\);" bound t) + (match-string 1))) + (pos1 (point))) + (if (re-search-forward "\a\\|\e\\\\" bound t) + (let ((text (buffer-substring-no-properties + pos1 (match-beginning 0)))) + (setq comint-osc--marker nil) + (delete-region pos0 (point)) + (when-let ((fun (cdr (assoc-string code comint-osc-handlers)))) + (funcall fun code text))) + (put-text-property pos0 bound 'invisible t) + (setq comint-osc--marker (copy-marker pos0)))))))) + +;; Hyperlink handling (OSC 8) + +(defvar comint-osc-hyperlink-map + (let ((map (make-sparse-keymap))) + (define-key map "\C-c\r" 'browse-url-button-open) + (define-key map [mouse-2] 'browse-url-button-open) + (define-key map [follow-link] 'mouse-face) + map) + "Keymap used by OSC 8 hyperlink buttons.") + +(define-button-type 'comint-osc-hyperlink + 'keymap comint-osc-hyperlink-map + 'help-echo (lambda (_ buffer pos) + (when-let ((url (get-text-property pos 'browse-url-data buffer))) + (format "mouse-2, C-c RET: Open %s" url)))) + +(defvar-local comint-osc-hyperlink--state nil) + +(defun comint-osc-hyperlink-handler (_ text) + "Create a hyperlink from an OSC 8 escape sequence. +This function is intended to be included as an entry of +`comint-osc-handlers'." + (when comint-osc-hyperlink--state + (let ((start (car comint-osc-hyperlink--state)) + (url (cdr comint-osc-hyperlink--state))) + (make-text-button start (point) + 'type 'comint-osc-hyperlink + 'browse-url-data url))) + (setq comint-osc-hyperlink--state + (and (string-match ";\\(.+\\)" text) + (cons (point-marker) (match-string-no-properties 1 text))))) + ;;; Converting process modes to use comint mode ;;============================================================================ diff --git a/lisp/net/browse-url.el b/lisp/net/browse-url.el index f739cd72cc3..c8ca70cc1f9 100644 --- a/lisp/net/browse-url.el +++ b/lisp/net/browse-url.el @@ -1780,6 +1780,7 @@ clickable and will use `browse-url' to open the URLs in question." category browse-url browse-url-data ,(match-string 0))))))) +;;;###autoload (defun browse-url-button-open (&optional external mouse-event) "Follow the link under point using `browse-url'. If EXTERNAL (the prefix if used interactively), open with the -- 2.39.2