From a04999011f303ec0940cf7edc19b1c98f7608f40 Mon Sep 17 00:00:00 2001 From: Akib Azmain Turja Date: Tue, 6 Dec 2022 21:30:01 +0600 Subject: [PATCH] Annotate shell prompts * eat.el (eat-enable-shell-prompt-annotation) (eat-shell-prompt-annotation-position) (eat-shell-prompt-annotation-running-margin-indicator) (eat-shell-prompt-annotation-success-margin-indicator) (eat-shell-prompt-annotation-failure-margin-indicator) (eat-shell-prompt-annotation-delay): New user option. * eat.el (eat-shell-prompt-annotation-running) (eat-shell-prompt-annotation-success) (eat-shell-prompt-annotation-failure): New face. * eat.el (eat--shell-command-status, eat--shell-prompt-begin) (eat--shell-prompt-mark, eat--shell-prompt-mark-overlays): New variable. * eat.el (eat--pre-prompt, eat--post-prompt) (eat--update-shell-prompt-mark-overlays, eat--pre-cmd) (eat--set-cmd-status): New function. * eat.el (eat-mode): Make 'eat--shell-command-status', 'eat--shell-prompt-begin', 'eat--shell-prompt-mark', 'eat--shell-prompt-mark-overlays' and 'eat--shell-prompt-annotation-update-timer' local. Set margin width if shell prompt annotation is enabled. * eat.el (eat--shell-prompt-annotation-update-timer): New variable. * eat.el (eat--process-output-queue): Call or schedule 'eat--update-shell-prompt-mark-overlays' call. * eat.el (eat--filter): Cancel timer 'eat--shell-prompt-annotation-update-timer'. * eat.el (eat-exec): Set prompt start & end and command start & end functions. --- eat.el | 252 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 235 insertions(+), 17 deletions(-) diff --git a/eat.el b/eat.el index 2c2908b..824277e 100644 --- a/eat.el +++ b/eat.el @@ -161,6 +161,63 @@ the history of commands like `eat', `shell-command' and :group 'eat-ui :group 'eat-eshell) +(defcustom eat-enable-shell-prompt-annotation t + "Non-nil means annotate shell prompt with the status of command. + +When non-nil, display a mark in front of shell prompt describing the +status of the command executed in that prompt." + :type 'boolean + :group 'eat-ui) + +(defcustom eat-shell-prompt-annotation-position 'left-margin + "The position where to display shell prompt annotation. + +The value can be one of the following: + +`left-margin' Use the left margin. +`right-margin' Use the right margin." + :type '(choice (const :tag "Left margin" left-margin) + (const :tag "Right margin" right-margin)) + :group 'eat-ui) + +(defcustom eat-shell-prompt-annotation-running-margin-indicator "-" + "String in margin annotation to indicate the command is running." + :type 'string + :group 'eat-ui) + +(defface eat-shell-prompt-annotation-running + '((t :inherit compilation-info)) + "Face used in annotation to indicate the command is running." + :group 'eat-ui) + +(defcustom eat-shell-prompt-annotation-success-margin-indicator "0" + "String in margin annotation to indicate the command has succeeded." + :type 'string + :group 'eat-ui) + +(defface eat-shell-prompt-annotation-success + '((t :inherit success)) + "Face used in annotation to indicate the command has succeeded." + :group 'eat-ui) + +(defcustom eat-shell-prompt-annotation-failure-margin-indicator "X" + "String in margin annotation to indicate the command has failed." + :type 'string + :group 'eat-ui) + +(defface eat-shell-prompt-annotation-failure + '((t :inherit error)) + "Face used in annotation to indicate the command has failed." + :group 'eat-ui) + +(defcustom eat-shell-prompt-annotation-delay 0.1 + "Seconds to wait before updating prompt annotations. + +Nil means update immediately." + :type '(choice (const :tag "Immediately" nil) + number) + :group 'eat-ui) + (defconst eat--cursor-type-value-type (let ((cur-type '(choice @@ -4112,6 +4169,18 @@ return \"eat-color\", otherwise return \"eat-mono\"." (defvar eat--synchronize-scroll-function nil "Function to synchronize scrolling between terminal and window.") +(defvar eat--shell-command-status 0 + "If the current shell command has finished, its exit status.") + +(defvar eat--shell-prompt-begin nil + "Beginning of last shell prompt.") + +(defvar eat--shell-prompt-mark nil + "Display property used to put a mark before the previous prompt.") + +(defvar eat--shell-prompt-mark-overlays nil + "List of overlay used to put marks before shell prompts.") + (defun eat-reset () "Perform a terminal reset." (interactive) @@ -4170,11 +4239,118 @@ If HOST isn't the host Emacs is running on, don't do anything." (ignore-errors (cd-absolute cwd)))) +(defun eat--pre-prompt (_) + "Save the beginning position of shell prompt." + (when eat-enable-shell-prompt-annotation + (setq eat--shell-prompt-begin (point-marker)))) + +(defun eat--post-prompt (_) + "Put a mark in the marginal area on current line." + (when eat-enable-shell-prompt-annotation + (let ((indicator + (if (zerop eat--shell-command-status) + (propertize + eat-shell-prompt-annotation-success-margin-indicator + 'face 'eat-shell-prompt-annotation-success) + (propertize + eat-shell-prompt-annotation-failure-margin-indicator + 'face 'eat-shell-prompt-annotation-failure)))) + ;; Update previous prompt's indicator using side-effect. + (when eat--shell-prompt-mark + (setf (cadr eat--shell-prompt-mark) indicator) + (setq eat--shell-prompt-mark nil)) + ;; Show this prompt's indicator. + (when eat--shell-prompt-begin + (when (< eat--shell-prompt-begin (point)) + ;; Save it, we'll use side-effect. + (setq eat--shell-prompt-mark + `((margin ,eat-shell-prompt-annotation-position) + ,indicator)) + ;; Make overlay and put bookkeeping properties. + (let ((identifier (gensym "eat--prompt-mark-identifier-")) + (before-str + (propertize " " 'display eat--shell-prompt-mark)) + (ov (make-overlay eat--shell-prompt-begin + (1+ eat--shell-prompt-begin)))) + (overlay-put ov 'before-string before-str) + (overlay-put ov 'eat--shell-prompt-mark-id identifier) + (add-text-properties + eat--shell-prompt-begin (1+ eat--shell-prompt-begin) + (list 'eat--before-string before-str + 'eat--shell-prompt-mark-id identifier + 'eat--shell-prompt-mark-overlay ov)) + (push ov eat--shell-prompt-mark-overlays))) + (setq eat--shell-prompt-begin nil))))) + +(defun eat--update-shell-prompt-mark-overlays (buffer) + "Update all overlays used to add mark before shell prompt. + +BUFFER is the terminal buffer." + (when (and (buffer-live-p buffer) + eat-enable-shell-prompt-annotation) + (with-current-buffer buffer + (while-no-input + ;; Delete all outdated overlays. + (dolist (ov eat--shell-prompt-mark-overlays) + (unless (eq (overlay-get ov 'eat--shell-prompt-mark-id) + (get-text-property (overlay-start ov) + 'eat--shell-prompt-mark-id)) + (delete-overlay ov) + (setq eat--shell-prompt-mark-overlays + (delq ov eat--shell-prompt-mark-overlays)))) + (save-excursion + ;; Recreate overlays if needed. + (goto-char (eat-term-beginning eat--terminal)) + (while (< (point) (eat-term-end eat--terminal)) + (when (get-text-property + (point) 'eat--shell-prompt-mark-id) + (let ((ov (get-text-property + (point) 'eat--shell-prompt-mark-overlay))) + (unless + (and ov + (overlay-buffer ov) + (eq (overlay-get + ov 'eat--shell-prompt-mark-id) + (get-text-property + (point) 'eat--shell-prompt-mark-id))) + ;; Recreate. + (when ov + (delete-overlay ov) + (setq eat--shell-prompt-mark-overlays + (delq ov eat--shell-prompt-mark-overlays))) + (setq ov (make-overlay (point) (1+ (point)))) + (overlay-put ov 'before-string + (get-text-property + (point) 'eat--before-string)) + (overlay-put ov 'eat--shell-prompt-mark-id + (get-text-property + (point) 'eat--shell-prompt-mark-id)) + (push ov eat--shell-prompt-mark-overlays)))) + (goto-char (or (next-single-property-change + (point) 'eat--shell-prompt-mark-id nil + (eat-term-end eat--terminal)) + (eat-term-end eat--terminal))))))))) + (defun eat--set-cmd (_ cmd) "Add CMD to `shell-command-history'." (when eat-enable-shell-command-history (add-to-history 'shell-command-history cmd))) +(defun eat--pre-cmd (_) + "Update shell prompt mark to indicate command is running." + (when (and eat-enable-shell-prompt-annotation + eat--shell-prompt-mark) + (setf (cadr eat--shell-prompt-mark) + (propertize + eat-shell-prompt-annotation-running-margin-indicator + 'face 'eat-shell-prompt-annotation-running)))) + +(defun eat--set-cmd-status (_ code) + "Set CODE as the current shell command's exit status." + (when eat-enable-shell-prompt-annotation + ;; We'll update the mark later when the prompt appears. + (setq eat--shell-command-status code))) + ;;;;; Input. @@ -4546,21 +4722,27 @@ END if it's safe to do so." (define-derived-mode eat-mode fundamental-mode "Eat" "Major mode for Eat." :group 'eat-ui - (mapc #'make-local-variable '(buffer-read-only - buffer-undo-list - filter-buffer-substring-function - mode-line-process - mode-line-buffer-identification - glyphless-char-display - cursor-type - track-mouse - eat--terminal - eat--process - eat--synchronize-scroll-function - eat--mouse-grabbing-type - eat--pending-output-chunks - eat--output-queue-first-chunk-time - eat--process-output-queue-timer)) + (mapc #'make-local-variable + '(buffer-read-only + buffer-undo-list + filter-buffer-substring-function + mode-line-process + mode-line-buffer-identification + glyphless-char-display + cursor-type + track-mouse + eat--terminal + eat--process + eat--synchronize-scroll-function + eat--mouse-grabbing-type + eat--shell-command-status + eat--shell-prompt-begin + eat--shell-prompt-mark + eat--shell-prompt-mark-overlays + eat--pending-output-chunks + eat--output-queue-first-chunk-time + eat--process-output-queue-timer + eat--shell-prompt-annotation-update-timer)) ;; This is intended; input methods don't work on read-only buffers. (setq buffer-read-only nil buffer-undo-list t @@ -4634,7 +4816,24 @@ mouse-3: Switch to char mode" ;; that would break the display. (eat--setup-glyphless-chars) (when eat-enable-blinking-text - (eat-blink-mode +1))) + (eat-blink-mode +1)) + (when eat-enable-shell-prompt-annotation + (let ((margin-width + (max + (string-width + eat-shell-prompt-annotation-running-margin-indicator) + (string-width + eat-shell-prompt-annotation-success-margin-indicator) + (string-width + eat-shell-prompt-annotation-failure-margin-indicator)))) + (pcase-exhaustive eat-shell-prompt-annotation-position + ('left-margin + (setq left-margin-width margin-width)) + ('right-margin + (setq right-margin-width margin-width)))) + ;; Make sure the marginal area is resized. + (dolist (win (get-buffer-window-list)) + (set-window-buffer win (current-buffer))))) ;;;;; Process Handling. @@ -4653,6 +4852,9 @@ The output chunks are pushed, so last output appears first.") (defvar eat--process-output-queue-timer nil "Timer to process output queue.") +(defvar eat--shell-prompt-annotation-update-timer nil + "Timer to update shell prompt annotations.") + (defun eat-kill-process () "Kill Eat process in current buffer." (interactive) @@ -4708,6 +4910,13 @@ OS's." (max (point-min) (- (eat-term-display-beginning eat--terminal) eat-term-scrollback-size)))) + (if (null eat-shell-prompt-annotation-delay) + (eat--update-shell-prompt-mark-overlays buffer) + (setq eat--shell-prompt-annotation-update-timer + (run-with-timer + eat-shell-prompt-annotation-delay + nil #'eat--update-shell-prompt-mark-overlays + buffer))) (when synchronize-scroll (funcall eat--synchronize-scroll-function)))))) @@ -4717,6 +4926,8 @@ OS's." (with-current-buffer (process-buffer process) (when eat--process-output-queue-timer (cancel-timer eat--process-output-queue-timer)) + (when eat--shell-prompt-annotation-update-timer + (cancel-timer eat--shell-prompt-annotation-update-timer)) (unless eat--output-queue-first-chunk-time (setq eat--output-queue-first-chunk-time (current-time))) (push output eat--pending-output-chunks) @@ -4816,7 +5027,14 @@ same Eat buffer. The hook `eat-exec-hook' is run after each exec." #'eat--manipulate-kill-ring (eat-term-ring-bell-function eat--terminal) #'eat--bell (eat-term-set-cwd-function eat--terminal) #'eat--set-cwd - (eat-term-set-cmd-function eat--terminal) #'eat--set-cmd) + (eat-term-prompt-start-function eat--terminal) + #'eat--pre-prompt + (eat-term-prompt-end-function eat--terminal) + #'eat--post-prompt + (eat-term-set-cmd-function eat--terminal) #'eat--set-cmd + (eat-term-cmd-start-function eat--terminal) #'eat--pre-cmd + (eat-term-cmd-finish-function eat--terminal) + #'eat--set-cmd-status) ;; Crank up a new process. (let* ((size (eat-term-size eat--terminal)) (process-environment