ambevar-dotfiles/.emacs.d/lisp/init-eshell.el

304 lines
12 KiB
EmacsLisp

;;; Eshell
;;; Eshell gets initialized differently. When eshell.el first gets loaded, only
;;; the core is defined and `eshell-load-hook' is called. For every Eshell
;;; session, `eshell-mode' is run: it resets `eshell-mode-map', it loads
;;; modules, runs their hooks and concludes with `eshell-first-time-mode-hook'
;;; (for the first session only) and `eshell-mode-hook'.
;; Emacs pinentry for GPG.
(require 'main)
;;; REVIEW: If history contains read-only text (e.g. accidental insertion of the prompt),
;;; `eshell-write-history' won't work.
;;; See #29153.
;;; REVIEW: Sometimes transmission-daemon does not start from Eshell.
;;; See #30465.
;;; REVIEW: Redirecting big output to file (e.g. /dev/null) is extremely slow.
;; > cat /usr/share/dict/british-english | wc -l
;;; The above line yields rancom results. Plus it's much slower than
;; > wc -l /usr/share/dict/british-english
;;; while it should only cost an additional process to launch.
;;; See #29154.
;;; REVIEW: Cannot "C-c C-c" during a `sudo pacman -Syu`. A bug was filed about that already.
;;; TODO: The buffer stutters when writing "in-place", e.g. pacman, git.
;;; It seems that it does not do it as much in `emacs -Q`.
;;; REVIEW: `eshell/sudo' should not read -u in command arguments.
;;; This fails: sudo pacman -Syu --noconfirm.
;;; http://www.gnu.org/software/emacs/manual/html_node/eshell/Built_002dins.html
;;; https://emacs.stackexchange.com/questions/5608/how-to-let-eshell-remember-sudo-password-for-two-minutes
;;; See http://debbugs.gnu.org/cgi/bugreport.cgi?bug=27411
;;; and #28323.
;;; REVIEW: Eshell/Shell completion fails when PATH has a non-readable element.
;;; See https://github.com/emacs-helm/helm/issues/1785
;;; and https://debbugs.gnu.org/cgi/bugreport.cgi?bug=27300.
;;; REVIEW: 40M+ output: Stack overflow in regexp matcher
;;; See bug#28329.
;;; I guess the chunking is significant too. Could you try saving
;;; the output chunks, with this:
;;
;; (defvar eshell-chunk-number 0)
;; (defconst eshell-output-chunk-dir "eshell-output")
;; (make-directory eshell-output-chunk-dir t)
;;
;; (defun catch-eshell-output-chunk ()
;; (write-region eshell-last-output-block-begin
;; eshell-last-output-end
;; (format "%s/chunk.%d"
;; eshell-output-chunk-dir
;; eshell-chunk-number)
;; nil :quiet)
;; (setq eshell-chunk-number (1+ eshell-chunk-number)))
;;
;; (add-hook 'eshell-output-filter-functions
;; 'catch-eshell-output-chunk)
;;;
;;; And then afterwards 'cat eshell-output/chunk.*' should hopefully
;;; reproduce it?
;;; REVIEW: Eshell mixes stderr and stdout it seems.
;;; Example:
;;; $ mu find --nocolor --sortfield=d --maxnum=500 flag:unread AND NOT flag:trashed >/dev/null
;;; mu: no matches for search expression (4)
;;; See #21605 "24.3; Eshell not using stderr".
;;; REVIEW: eshell/date does not support many options from GNU date.
;;; > date +%Z
;;; See #29157.
(require 'patch-eshell)
;;; TODO: Change face of eshell/* commands in commandline to avoid confusion
;;; with system programs.
;;; TODO: Merge/re-use documentation of eshell/* commands with their docstring.
;;; TODO: Hour is printed twice. We don't need to set this?
;; (setq eshell-ls-date-format (replace-regexp-in-string "^\\+*" "" (getenv "TIME_STYLE")))
;;; TODO: ls: Sort using locale.
;;; REVIEW: `kill -#' does not work.
;;; See #29156.
;;; Use TRAMP to use Eshell as root.
(require 'em-tramp)
(setq password-cache t)
(setq password-cache-expiry 3600)
(with-eval-after-load 'esh-module
;; REVIEW: It used to work, but now the early `provide' seems to backfire.
(unless (boundp 'eshell-modules-list)
(load "esh-module"))
;; Don't print the banner.
(delq 'eshell-banner eshell-modules-list)
(push 'eshell-tramp eshell-modules-list))
(setq
eshell-ls-use-colors t
;; ffap-shell-prompt-regexp changes the behaviour of `helm-find-files' when
;; point is on prompt. I find this disturbing.
ffap-shell-prompt-regexp nil
eshell-history-size 1024
eshell-hist-ignoredups t
eshell-destroy-buffer-when-process-dies t)
(with-eval-after-load 'em-term
(dolist (p '("alsamixer" "gtypist" "htop" "mpsyt" "mpv" "watch"))
(add-to-list 'eshell-visual-commands p))
(setq eshell-visual-subcommands
;; Some Git commands use a pager by default.
;; Either invoke the subcommands in a term ("visual") or configure Git
;; to disable the pager globally.
;; '(("git" "log" "diff" "show")
'(("sudo" "vi" "visudo"))))
;;; Alias management possibilities:
;;; - Version eshell-alias and store it in user-emacs-directory. Simplest and
;;; fastest, but aliases cannot be included conditionnaly, e.g. depending on the
;;; existence of a program.
;;; - Store eshell-alias in cache and populate it dynamically on startup.
;; (setq eshell-aliases-file (concat user-emacs-directory "eshell-alias"))
;;;
;;; `eshell/alias' is too slow as it reads and write the file on each definition.
;;; Let's write manually instead.
(with-eval-after-load 'em-alias
;;; If we read the alias list here, it means we make commandline-defined aliases persistent.
;; (eshell-read-aliases-list)
(dolist
(alias
'(("l" "ls -1 $*")
("la" "ls -lAh $*")
("ll" "ls -lh $*")
;; TODO: Aliasing eshell/{cp,mv,ln} does not work.
;; REVIEW: Eshell/TRAMP's sudo does not work with aliases.
;; See #28320, #27168.
;; ("ls" "ls -F $*") ; not supported
;; ("emacs" "find-file $1")
;; ("cp" "eshell/cp -iv $*")
;; ("mv" "eshell/mv -iv $*")
("cpv" "cp -iv $*")
("mvv" "mv -iv $*")
("rmv" "rm -v $*")
("md" "eshell/mkdir -p $*")
("mkcd" "eshell/mkdir -p $* ; cd $1"))) ; TODO: '&&' does not work because mkdir exits with nil?
(add-to-list 'eshell-command-aliases-list alias))
(eshell-write-aliases-list))
;;; Hooks
;;; `nobreak-char-display' makes some output look weird, e.g. with 'tree'.
(add-hook 'eshell-mode-hook 'ambrevar/turn-off-nobreak-char-display)
;;; History
;;; REVIEW: history: do not save failed Eshell commands (See `eshell-last-command-status')
;;; Eshell commands always return 0.
(setq eshell-input-filter
(lambda (str)
(not (or
;; Here we can filter out failing commands. This is usually a bad
;; idea since a lot of useful commands have non-zero exit codes
;; (including Emacs/Eshell functions).
;; (/= eshell-last-command-status 0)
(string= "" str)
(string= "cd" str)
(string-prefix-p "cd " str)
;; Filter out space-beginning commands from history.
(string-prefix-p " " str)))))
;;; Shared history.
(defvar ambrevar/eshell-history-global-ring nil
"The history ring shared across Eshell sessions.")
(defun ambrevar/eshell-hist-use-global-history ()
"Make Eshell history shared across different sessions."
(unless ambrevar/eshell-history-global-ring
(when eshell-history-file-name
(eshell-read-history nil t))
(setq ambrevar/eshell-history-global-ring (or eshell-history-ring (make-ring eshell-history-size))))
(setq eshell-history-ring ambrevar/eshell-history-global-ring))
(add-hook 'eshell-mode-hook 'ambrevar/eshell-hist-use-global-history)
(defun ambrevar/eshell-history-remove-duplicates ()
(require 'functions) ; For `ambrevar/ring-delete-first-item-duplicates'.
(ambrevar/ring-delete-first-item-duplicates eshell-history-ring))
(add-hook 'eshell-pre-command-hook 'ambrevar/eshell-history-remove-duplicates)
;; Always save history
(add-hook 'eshell-pre-command-hook 'eshell-save-some-history)
;;; Version and encrypt history.
;; TODO: The following makes EPA always prompt for recipients for some reason. Bug?
;; (make-directory "~/projects/personal/history/" :parents)
;; (setq eshell-history-file-name (expand-file-name "eshell.gpg" "~/projects/personal/history"))
;; (defun ambrevar/fix-local-epa-file-encrypt-to ()
;; (unless (local-variable-p 'epa-file-encrypt-to (current-buffer))
;; (make-local-variable 'epa-file-encrypt-to))
;; (setq epa-file-encrypt-to "mail@ambrevar.xyz"))
;; (add-hook 'eshell-mode-hook 'ambrevar/fix-local-epa-file-encrypt-to)
(when (require 'helm nil :noerror)
(defun ambrevar/helm/eshell-set-keys ()
(define-key eshell-mode-map [remap eshell-pcomplete] 'helm-esh-pcomplete)
(define-key eshell-mode-map (kbd "M-p") 'helm-eshell-history)
(define-key eshell-mode-map (kbd "M-s") nil) ; Useless when we have 'helm-eshell-history.
(define-key eshell-mode-map (kbd "M-s f") 'helm-eshell-prompts-all))
(add-hook 'eshell-mode-hook 'ambrevar/helm/eshell-set-keys))
;;; Auto-suggestion
(when (require 'esh-autosuggest nil t)
(setq esh-autosuggest-delay 0.75)
(add-hook 'eshell-mode-hook 'esh-autosuggest-mode)
(define-key esh-autosuggest-active-map (kbd "<tab>") 'company-complete-selection)
(when (require 'helm-config nil t)
;; TODO: Why is this in esh-autosuggest? Move to toplevel.
(define-key company-active-map (kbd "M-p") 'helm-eshell-history)))
;;; Detach
(when (require 'package-eshell-detach nil t)
(defun ambrevar/eshell-detach-set-keys ()
(define-key eshell-mode-map (kbd "C-c C-z") 'eshell-detach-stop)
(define-key eshell-mode-map (kbd "S-<return>") 'eshell-detach-send-input)
(define-key eshell-mode-map (kbd "C-<return>") 'eshell-detach-attach))
(add-hook 'eshell-mode-hook 'ambrevar/eshell-detach-set-keys))
;; Man
(when (string= (file-symlink-p (executable-find "man")) "mandoc")
;; Some systems like Void Linux use mandoc instead of man and do not know the
;; --nj, --nh flags.
(defun ambrevar/pcmpl-args-mandoc-man-function (name)
(let ((process-environment process-environment))
;; Setting MANWIDTH to a high number makes most paragraphs fit on a single
;; line, reducing the number of false positives that result from lines
;; starting with `-' that aren't really options.
(push "MANWIDTH=10000" process-environment)
(pcmpl-args-process-file "man" "--" name)))
(setq pcmpl-args-man-function 'ambrevar/pcmpl-args-mandoc-man-function))
;; Completion
(when (require 'bash-completion nil t)
(when (and (or (executable-find "fish")
;; "fish" needs not be in PATH with guix.
(executable-find "guix"))
(require 'fish-completion nil t))
(setq fish-completion-fallback-on-bash-p t)
(global-fish-completion-mode)))
;; Misc.
(defun ambrevar/eshell-append-region-to-command-line (begin end)
(interactive "r")
(require 'subr-x) ; For `string-trim'.
(save-mark-and-excursion
(let ((string (buffer-substring-no-properties begin end)))
(setq string (string-trim string))
(setq string (concat string " "))
(goto-char (point-max))
(ignore-errors (cycle-spacing 0))
(insert string))))
;; TODO: Fix eshell-did-you-mean.
;; (when (require 'eshell-did-you-mean nil 'noerror)
;; (eshell-did-you-mean-setup))
(when (require 'eshell-prompt-extras nil 'noerror)
;; Leave `eshell-highlight-prompt' to t as it sets the read-only property.
(setq epe-path-style 'full)
(setq eshell-prompt-function #'epe-theme-multiline-with-status))
(when (require 'helm-fish-completion nil 'noerror)
(define-key shell-mode-map (kbd "<tab>") 'helm-fish-completion)
(setq helm-esh-pcomplete-build-source-fn #'helm-fish-completion-make-eshell-source))
(require 'patch-eshell-inside-emacs) ; Still required as of Emacs 27.1.
(defun ambrevar/eshell-narrow-to-prompt ()
"Narrow buffer to prompt at point."
(interactive)
(narrow-to-region
(save-excursion
(forward-line)
(call-interactively #'eshell-previous-prompt)
(beginning-of-line)
(point))
(save-excursion
(forward-line)
(call-interactively #'eshell-next-prompt)
(re-search-backward eshell-prompt-regexp nil t)
(when (and (require 'eshell-prompt-extras nil 'noerror)
(eq eshell-prompt-function #'epe-theme-multiline-with-status))
(previous-line))
(point))))
(defun ambrevar/eshell-set-keys ()
(define-key eshell-mode-map (kbd "C-x n d") 'ambrevar/eshell-narrow-to-prompt))
(add-hook 'eshell-mode-hook 'ambrevar/eshell-set-keys)
(provide 'init-eshell)