;;; shell.el -*- lexical-binding: t; -*- ;;; ;;; Author: Jakob L. Kreuze ;;; URL: https://git.sr.ht/~jakob/.emacs.d/ ;;; ;;; After reading Pierre Neidhardt's article on `shell-mode', I've decided to ;;; give it a try over Alacritty. This is a configuration to make it act a ;;; little bit more like `eshell' -- a notion of "visual" programs that need to ;;; be run in a proper terminal emulator, and popping the shell buffer to the ;;; currently-focused window. ;;; ;;; [1]: https://ambrevar.xyz/emacs-eshell-versus-shell/index.html ;;; (defmacro with-temporary-bindings (bindings &rest form) "Weak and partially underspecified macro to temporarily set `bindings', undoing them when `form' has finished evaluating." `(unwind-protect (progn ,@(mapcar #'(lambda (form) (cons 'setq form)) bindings) ,@form) (progn ,@(mapcar #'(lambda (form) `(setq ,(car form) ,(eval (car form)))) bindings)))) ;; I dislike the default behavior of creating a new window upon invoking ;; `shell', but there isn't a better way of dealing with that. (defun shell-same-window (&optional buffer) "Copy-pasted version of `shell' which does not use `pop-to-buffer'." (interactive) (let ((buffer-name "*shell*") (n 1)) (while (get-buffer buffer-name) (setq buffer-name (format "*shell-%d*" n)) (setq n (1+ n))) (setq buffer (switch-to-buffer buffer-name))) ;; Don't play 'dumb', but don't ruin it for other modes that use comint. ;; (make-local-variable 'comint-terminfo-terminal) ;; (setq comint-terminfo-terminal "xterm-256color") (with-connection-local-variables ;; On remote hosts, the local `shell-file-name' might be useless. (when (file-remote-p default-directory) (if (and (called-interactively-p 'any) (null explicit-shell-file-name) (null (getenv "ESHELL"))) (set (make-local-variable 'explicit-shell-file-name) (file-local-name (expand-file-name (read-file-name "Remote shell path: " default-directory shell-file-name t shell-file-name)))))) ;; Rain or shine, BUFFER must be current by now. (unless (comint-check-proc buffer) (let* ((prog (or explicit-shell-file-name (getenv "ESHELL") shell-file-name)) (name (file-name-nondirectory prog)) (startfile (concat "~/.emacs_" name)) (xargs-name (intern-soft (concat "explicit-" name "-args")))) (unless (file-exists-p startfile) (setq startfile (concat user-emacs-directory "init_" name ".sh"))) (setq-local shell--start-prog (file-name-nondirectory prog)) (setq comint-terminfo-terminal "xterm-256color") (with-temporary-bindings ((comint-terminfo-terminal "xterm-256color")) (apply #'make-comint-in-buffer "shell" buffer prog (if (file-exists-p startfile) startfile) (if (and xargs-name (boundp xargs-name)) (symbol-value xargs-name) '("-i")))) (shell-mode)))) buffer) ;; Fortunately, there is a better way of doing it for `man' (which we'll see in ;; the configuration later). (setq Man-notify-method 'pushy) ;; Use ANSI colors in `shell' (and elsewhere!?). (use-package xterm-color ;; Use the same colors as Alacritty. :init (setq xterm-color-names ["#333333" "#F2777A" "#99CC99" "#FFCC66" "#6699CC" "#CC99CC" "#66CCCC" "#DDDDDD"]) (setq xterm-color-names-bright ["#5D5D5D" "#CF8BA2" "#90D6A6" "#E0BF80" "#5F7AB1" "#807793" "#1FB6D3" "#F6F6F6"]) :init (setq comint-output-filter-functions (remove 'ansi-color-process-output comint-output-filter-functions)) :init (defun xterm-title-filter (string) "Pre-match filter for setting a buffer-local variable `shell-title' upon reading the ANSI escape sequence for setting the terminal emulator's window title." (let ((pattern (rx "\033]0;" (group-n 1 (one-or-more (not "\007"))) "\007"))) (when (string-match pattern string) (setq-local shell-title (match-string 1 string)))) string) :hook (shell-mode . (lambda () ;; Disable font-locking in this buffer to improve performance (font-lock-mode -1) ;; Prevent font-locking from being re-enabled in this buffer (make-local-variable 'font-lock-function) (setq font-lock-function (lambda (_) nil)) (add-hook 'comint-preoutput-filter-functions 'xterm-color-filter nil t) (add-hook 'comint-preoutput-filter-functions 'xterm-title-filter nil t)))) (defvar visual-commands '("alsamixer" "r2" "radare2" "top") "Shell commands to run in a proper terminal emulator.") (defun shell-fake-comint-send (pmark) "Make it seem as though the current line was sent to the inferior." ;; First, we make the "input" to comint empty. (goto-char pmark) (kill-line) (comint-send-input) ; and send it. (save-excursion ;; Then, we copy the input to the previous line, (forward-line -1) (end-of-visual-line) (yank) ;; And finish by cleaning the kill ring. (pop kill-ring) (when kill-ring-yank-pointer (setq kill-ring-yank-pointer kill-ring)))) (defun shell-screen-command () "Handle any visual commands before deferring to the inferior." (interactive) (let ((proc (get-buffer-process (current-buffer)))) (if (not proc) (user-error "Current buffer has no process") (let* ((pmark (process-mark proc)) (intxt (if (>= (point) (marker-position pmark)) (progn (if comint-eol-on-send (if comint-use-prompt-regexp (end-of-line) (goto-char (field-end)))) (buffer-substring pmark (point))) (let ((copy (funcall comint-get-old-input))) (goto-char pmark) (insert copy) copy))) (input (if (not (eq comint-input-autoexpand 'input)) ;; Just whatever's already there. intxt ;; Expand and leave it visible in buffer. (comint-replace-by-expanded-history t pmark) (buffer-substring pmark (point)))) (command (cl-first (split-string input " "))) (args (cl-rest (split-string input " "))) (visual (cl-member command visual-commands :test #'string=))) (cond ((string= command "man") (man (string-join args " ")) (shell-fake-comint-send pmark)) (visual (start-process "" nil "alacritty" "--class" (format "*%s*,*%s*" command command) "-e" "bash" "-c" (format "echo -ne \"\033]0;%s\007\" && %s" input input)) (shell-fake-comint-send pmark)) (t (comint-send-input))))))) (bind-key "RET" 'shell-screen-command shell-mode-map) (defun shell-clear () "Remove all preceding lines of shell interaction." (interactive) (let ((n (* 2 (1- (line-number-at-pos))))) (goto-char (point-min)) (while (cl-plusp n) (delete-visual-line) (setq n (1- n))) (end-of-line))) (bind-key "C-c C-l" 'shell-clear shell-mode-map) (bind-key "C-c C-s" 'comint-history-isearch shell-mode-map) (bind-key "C-c C-r" 'comint-history-isearch-backward shell-mode-map) ;; Close the buffer upon quitting with C-d. (defun shell-kill-buffer-on-quit (process event) "Called when the shell process is stopped. Kills the buffer when the shell program quits." (ignore event) (comint-write-input-ring) (let ((buf (process-buffer process))) (when (buffer-live-p buf) (with-current-buffer buf (kill-buffer))))) (defun add-shell-sentinel (&rest args) (ignore args) (set-process-sentinel (get-buffer-process (current-buffer)) #'shell-kill-buffer-on-quit)) (add-hook 'shell-mode-hook #'add-shell-sentinel) ;; Use proper Bash completion. (use-package bash-completion :hook (shell-mode . (lambda () (company-mode) (setq completion-at-point-functions (list #'bash-completion-dynamic-complete)) (bind-key "TAB" #'company-complete shell-mode-map))) ;; AWS is sometimes a little slow... :init (setq bash-completion-process-timeout 5.0)) (add-to-list 'auto-mode-alist '("\\.ebuild?\\'" . sh-mode))