Move eshell prompt to its own file
This commit is contained in:
parent
c3a0ef4160
commit
7b9cc70e7c
214
elisp/eshell-starship.el
Normal file
214
elisp/eshell-starship.el
Normal file
@ -0,0 +1,214 @@
|
||||
;;; eshell-starship.el --- Starship-like (https://starship.rs) prompt for eshell
|
||||
;;; Commentary:
|
||||
;;; Code:
|
||||
|
||||
(require 'vc)
|
||||
(require 'vc-git)
|
||||
(require 'eshell)
|
||||
|
||||
(defun eshell-starship--git-process-lines (&rest flags)
|
||||
"Run `vc-git-program' and return an array of its output lines.
|
||||
FLAGS are passed to `vc-git-program' as its arguments."
|
||||
(with-temp-buffer
|
||||
(apply 'vc-git-command t 0 nil flags)
|
||||
(if (zerop (buffer-size))
|
||||
'()
|
||||
(string-lines (buffer-substring-no-properties 1 (buffer-size))))))
|
||||
|
||||
(defun eshell-starship--replace-home-with-tilda (path)
|
||||
"If PATH beings with $HOME (the environment variable), replace it with ~."
|
||||
(let ((home (getenv "HOME")))
|
||||
(if (equal home path)
|
||||
"~"
|
||||
(setq home (file-name-as-directory home))
|
||||
(if (string-prefix-p home path)
|
||||
(concat "~/" (seq-subseq path (length home)))
|
||||
path))))
|
||||
|
||||
(defun eshell-starship--prompt-cut-path (num path)
|
||||
"Cut PATH down to NUM components.
|
||||
Example:
|
||||
/this/is/a/path 3-> is/a/path"
|
||||
(let ((parts (string-split path "/" t nil)))
|
||||
(concat
|
||||
(when (and (file-name-absolute-p path)
|
||||
(not (equal "~" (car parts)))
|
||||
(<= (length parts) num))
|
||||
"/")
|
||||
(string-join (last parts num) "/"))))
|
||||
|
||||
(defun eshell-starship--prompt-get-dir ()
|
||||
"Get dir for `eshell-starship--prompt-function'."
|
||||
(eshell-starship--prompt-cut-path 3
|
||||
(if-let ((worktree (vc-root-dir))
|
||||
(parent (file-name-parent-directory worktree)))
|
||||
(file-relative-name default-directory parent)
|
||||
(eshell-starship--replace-home-with-tilda default-directory))))
|
||||
|
||||
(defun eshell-starship--prompt-status-char-for-branch (branch remote)
|
||||
"Get the status char representing the relation between BRANCH and REMOTE."
|
||||
(let ((lines (eshell-starship--git-process-lines
|
||||
"rev-list"
|
||||
"--left-right"
|
||||
(concat branch "..." remote)))
|
||||
(to-remote nil)
|
||||
(to-local nil))
|
||||
(dolist (line lines)
|
||||
(if-let (((not (string-empty-p line)))
|
||||
(dir-char (aref line 0)))
|
||||
(if (= dir-char ?<)
|
||||
(setq to-remote t)
|
||||
(setq to-local t))))
|
||||
(cond
|
||||
((and to-remote to-local) ?)
|
||||
(to-remote ?)
|
||||
(to-local ?))))
|
||||
|
||||
|
||||
(defun eshell-starship--prompt-current-branch-status ()
|
||||
"Get the status char for the current branch and its remote."
|
||||
(let ((refs (eshell-starship--git-process-lines
|
||||
"for-each-ref"
|
||||
"--format=%(HEAD)%00%(refname:short)%00%(upstream:short)"
|
||||
"refs/heads")))
|
||||
(catch 'break
|
||||
(dolist (ref refs)
|
||||
(if-let ((split-ref (split-string ref "\0" nil nil))
|
||||
((equal (car split-ref) "*")))
|
||||
(throw 'break (eshell-starship--prompt-status-char-for-branch
|
||||
(cadr split-ref)
|
||||
(caddr split-ref))))))))
|
||||
|
||||
(defun eshell-starship--prompt-git-state-chars ()
|
||||
"Get chars, like + and ✘ for `eshell-starship--prompt-function'."
|
||||
(let ((lines (eshell-starship--git-process-lines "status" "--porcelain=v1"))
|
||||
(branch-status (eshell-starship--prompt-current-branch-status))
|
||||
(status-arr))
|
||||
(dolist (line lines)
|
||||
(cl-loop with fields = (string-split line " " t " *")
|
||||
with status-str = (car-safe fields)
|
||||
for status-char across status-str
|
||||
do
|
||||
(cond ((or (= status-char ?M) (= status-char ?T))
|
||||
(add-to-list 'status-arr ?!))
|
||||
((= status-char ??)
|
||||
(add-to-list 'status-arr ??))
|
||||
((or (= status-char ?A) (= status-char ?C))
|
||||
(add-to-list 'status-arr ?+))
|
||||
((= status-char ?D)
|
||||
(add-to-list 'status-arr ?))
|
||||
((= status-char ?R)
|
||||
(add-to-list 'status-arr ?»))
|
||||
((= status-char ?U)
|
||||
(add-to-list 'status-arr ?=)))))
|
||||
(sort status-arr #'<)
|
||||
(when branch-status
|
||||
(push branch-status status-arr))
|
||||
(apply 'string status-arr)))
|
||||
|
||||
(defun eshell-starship--prompt-git-get-operation ()
|
||||
"Return the current git operation. For example, a revert."
|
||||
(let ((git-dir (expand-file-name ".git" (vc-git-root default-directory))))
|
||||
(cond
|
||||
((file-exists-p (expand-file-name "REVERT_HEAD" git-dir))
|
||||
"REVERTING")
|
||||
((file-exists-p (expand-file-name "rebase-merge" git-dir))
|
||||
"REBASING")
|
||||
((file-exists-p (expand-file-name "MERGE_HEAD" git-dir))
|
||||
"MERGING"))))
|
||||
|
||||
(defun eshell-starship--prompt-git-status ()
|
||||
"Get git status for `eshell-starship--prompt-function'."
|
||||
(let ((branch (car (vc-git-branches)))
|
||||
(state (eshell-starship--prompt-git-state-chars))
|
||||
(operation (eshell-starship--prompt-git-get-operation)))
|
||||
(concat
|
||||
(propertize (concat " " branch) 'face '(:foreground "medium purple"))
|
||||
(unless (string-empty-p state)
|
||||
(propertize (concat " [" state "]") 'face '(:foreground "red")))
|
||||
(when operation
|
||||
(concat " (" (propertize operation 'face
|
||||
'(:inherit 'bold :foreground "yellow")) ")")))))
|
||||
|
||||
(defun eshell-starship--prompt-vc-status ()
|
||||
"Get vc status for `eshell-starship--prompt-function'."
|
||||
(if-let (backend (vc-responsible-backend default-directory t))
|
||||
(if (eq backend 'Git)
|
||||
(eshell-starship--prompt-git-status)
|
||||
(propertize
|
||||
(concat " " (downcase (symbol-name backend)))
|
||||
'face '(:foreground "purple")))))
|
||||
|
||||
(defvar-local eshell-starship--prompt-last-start-time nil
|
||||
"Start time of last eshell command.")
|
||||
|
||||
(defun eshell-starship--prompt-timer-pre-cmd ()
|
||||
"Command run before each eshell program to record the time."
|
||||
(setq eshell-starship--prompt-last-start-time (current-time)))
|
||||
|
||||
(add-hook 'eshell-pre-command-hook #'eshell-starship--prompt-timer-pre-cmd)
|
||||
|
||||
(defun eshell-starship--prompt-format-span (span)
|
||||
"Format SPAN as \"XhXms\"."
|
||||
(let* ((hours (/ span 3600))
|
||||
(mins (% (/ span 60) 60))
|
||||
(secs (% span 60)))
|
||||
(concat (unless (= hours 0)
|
||||
(format "%dh" hours))
|
||||
(unless (= mins 0)
|
||||
(format "%dm" mins))
|
||||
(format "%ds" secs))))
|
||||
|
||||
(defun eshell-starship--prompt-last-command-time (end-time)
|
||||
"Return the prompt component for the time of the last command.
|
||||
END-TIME is the time when the command finished executing."
|
||||
(if-let ((eshell-starship--prompt-last-start-time)
|
||||
(len (time-subtract end-time
|
||||
eshell-starship--prompt-last-start-time))
|
||||
(float-len (float-time len))
|
||||
((< 3 float-len))
|
||||
(int-len (round float-len)))
|
||||
(concat " time "
|
||||
(propertize (eshell-starship--prompt-format-span int-len)
|
||||
'face '(:foreground "gold1")))))
|
||||
|
||||
(defun eshell-starship--prompt-function ()
|
||||
"Function for `eshell-prompt-function'."
|
||||
(let* ((end-time (current-time))
|
||||
(dir (eshell-starship--prompt-get-dir))
|
||||
(prompt (concat
|
||||
"\n"
|
||||
(propertize dir 'face '(:foreground "dark turquoise"))
|
||||
(unless (file-writable-p dir)
|
||||
" ")
|
||||
(eshell-starship--prompt-vc-status)
|
||||
(eshell-starship--prompt-last-command-time end-time)
|
||||
(propertize "\n" 'read-only t 'rear-nonsticky t)
|
||||
(propertize
|
||||
"❯ " 'face `(:foreground
|
||||
,(if (= eshell-last-command-status 0)
|
||||
"lime green"
|
||||
"red"))
|
||||
'rear-nonsticky t))))
|
||||
(setq eshell-starship--prompt-last-start-time nil)
|
||||
prompt))
|
||||
|
||||
(defvar-local ehsell-starship--restore-state nil
|
||||
"State of various variables set by `eshell-starship-prompt-mode'.")
|
||||
|
||||
;;;###autoload
|
||||
(define-minor-mode eshell-starship-prompt-mode
|
||||
"Minor mode to make eshell prompts look like starship (https://starship.rs)"
|
||||
:global nil
|
||||
:init-value nil
|
||||
:interactive (eshell-mode)
|
||||
(if eshell-starship-prompt-mode
|
||||
(setq-local eshell-starship--restore-state
|
||||
(buffer-local-set-state eshell-prompt-function
|
||||
'eshell-starship--prompt-function
|
||||
eshell-prompt-regexp "^❯ "
|
||||
eshell-highlight-prompt nil))
|
||||
(buffer-local-restore-state eshell-starship--restore-state)))
|
||||
|
||||
(provide 'eshell-starship)
|
||||
;;; eshell-starship.el ends here
|
174
init.el
174
init.el
@ -923,175 +923,7 @@ If no name is given, list all bookmarks instead."
|
||||
(eshell/cd (bookmark-get-filename name))
|
||||
(when my/eshell-bm-auto-ls
|
||||
(eshell/ls)))
|
||||
(eshell-print (string-join (bookmark-all-names) " "))))
|
||||
(defun my/-git-process-lines (&rest flags)
|
||||
"Run `vc-git-program' and return an array of its output lines."
|
||||
(with-temp-buffer
|
||||
(apply 'vc-git-command t 0 nil flags)
|
||||
(if (zerop (buffer-size))
|
||||
'()
|
||||
(string-lines (buffer-substring-no-properties 1 (buffer-size))))))
|
||||
(defun my/-replace-home-with-tilda (path)
|
||||
(let ((home (getenv "HOME")))
|
||||
(if (equal home path)
|
||||
"~"
|
||||
(setq home (file-name-as-directory home))
|
||||
(if (string-prefix-p home path)
|
||||
(concat "~/" (seq-subseq path (length home)))
|
||||
path))))
|
||||
(defun my/-eshell-prompt-cut-path (num path)
|
||||
"Cut PATH down to NUM components."
|
||||
(let ((parts (string-split path "/" t nil)))
|
||||
(concat
|
||||
(when (and (file-name-absolute-p path)
|
||||
(not (equal "~" (car parts)))
|
||||
(<= (length parts) num))
|
||||
"/")
|
||||
(string-join (last parts num) "/"))))
|
||||
(defun my/-eshell-prompt-get-dir ()
|
||||
"Get dir for `my/-eshell-prompt-function'"
|
||||
(my/-eshell-prompt-cut-path 3
|
||||
(if-let ((worktree (vc-root-dir))
|
||||
(parent (file-name-parent-directory worktree)))
|
||||
(file-relative-name default-directory parent)
|
||||
(my/-replace-home-with-tilda default-directory))))
|
||||
(defun my/-eshell-prompt-status-char-for-branch (branch remote)
|
||||
"Get the status char representing the relation between BRANCH and REMOTE."
|
||||
(let ((lines (my/-git-process-lines
|
||||
"rev-list"
|
||||
"--left-right"
|
||||
(concat branch "..." remote)))
|
||||
(to-remote nil)
|
||||
(to-local nil))
|
||||
(dolist (line lines)
|
||||
(if-let (((not (string-empty-p line)))
|
||||
(dir-char (aref line 0)))
|
||||
(if (= dir-char ?<)
|
||||
(setq to-remote t)
|
||||
(setq to-local t))))
|
||||
(cond
|
||||
((and to-remote to-local) ?)
|
||||
(to-remote ?)
|
||||
(to-local ?))))
|
||||
(defun my/-eshell-prompt-current-branch-status ()
|
||||
"Get the status char for the current branch and its remote."
|
||||
(let ((refs (my/-git-process-lines
|
||||
"for-each-ref"
|
||||
"--format=%(HEAD)%00%(refname:short)%00%(upstream:short)"
|
||||
"refs/heads")))
|
||||
(catch 'break
|
||||
(dolist (ref refs)
|
||||
(if-let ((split-ref (split-string ref "\0" nil nil))
|
||||
((equal (car split-ref) "*")))
|
||||
(throw 'break (my/-eshell-prompt-status-char-for-branch
|
||||
(cadr split-ref)
|
||||
(caddr split-ref))))))))
|
||||
(defun my/-eshell-prompt-git-state-chars ()
|
||||
"Get chars, like + and ✘ for `my/-eshell-prompt-function'."
|
||||
(let ((lines (my/-git-process-lines "status" "--porcelain=v1"))
|
||||
(branch-status (my/-eshell-prompt-current-branch-status))
|
||||
(status-arr))
|
||||
(dolist (line lines)
|
||||
(cl-loop with fields = (string-split line " " t " *")
|
||||
with status-str = (car-safe fields)
|
||||
for status-char across status-str
|
||||
do
|
||||
(cond ((or (= status-char ?M) (= status-char ?T))
|
||||
(add-to-list 'status-arr ?!))
|
||||
((= status-char ??)
|
||||
(add-to-list 'status-arr ??))
|
||||
((or (= status-char ?A) (= status-char ?C))
|
||||
(add-to-list 'status-arr ?+))
|
||||
((= status-char ?D)
|
||||
(add-to-list 'status-arr ?))
|
||||
((= status-char ?R)
|
||||
(add-to-list 'status-arr ?»))
|
||||
((= status-char ?U)
|
||||
(add-to-list 'status-arr ?=)))))
|
||||
(sort status-arr #'<)
|
||||
(when branch-status
|
||||
(push branch-status status-arr))
|
||||
(apply 'string status-arr)))
|
||||
(defun my/-eshell-prompt-git-get-operation ()
|
||||
"Return the current git operation. For example, a revert."
|
||||
(let ((git-dir (expand-file-name ".git" (vc-git-root default-directory))))
|
||||
(cond
|
||||
((file-exists-p (expand-file-name "REVERT_HEAD" git-dir))
|
||||
"REVERTING")
|
||||
((file-exists-p (expand-file-name "rebase-merge" git-dir))
|
||||
"REBASING")
|
||||
((file-exists-p (expand-file-name "MERGE_HEAD" git-dir))
|
||||
"MERGING"))))
|
||||
(defun my/-eshell-prompt-git-status ()
|
||||
"Get git status for `my/-eshell-prompt-function'"
|
||||
(let ((branch (car (vc-git-branches)))
|
||||
(state (my/-eshell-prompt-git-state-chars))
|
||||
(operation (my/-eshell-prompt-git-get-operation)))
|
||||
(concat
|
||||
(propertize (concat " " branch) 'face '(:foreground "medium purple"))
|
||||
(unless (string-empty-p state)
|
||||
(propertize (concat " [" state "]") 'face '(:foreground "red")))
|
||||
(when operation
|
||||
(concat " (" (propertize operation 'face
|
||||
'(:inherit 'bold :foreground "yellow")) ")")))))
|
||||
(defun my/-eshell-prompt-vc-status ()
|
||||
"Get vc status for `my/-eshell-prompt-function'."
|
||||
(if-let (backend (vc-responsible-backend default-directory t))
|
||||
(if (eq backend 'Git)
|
||||
(my/-eshell-prompt-git-status)
|
||||
(my/-eshell-prompt-set-face-color
|
||||
(concat " " (downcase (symbol-name backend)))
|
||||
"purple"))))
|
||||
(defvar-local my/-eshell-prompt-last-start-time nil
|
||||
"Start time of last eshell command.")
|
||||
(defun my/-eshell-prompt-timer-pre-cmd ()
|
||||
"Command run before each eshell program to record the time."
|
||||
(setq my/-eshell-prompt-last-start-time (current-time)))
|
||||
(add-hook 'eshell-pre-command-hook #'my/-eshell-prompt-timer-pre-cmd)
|
||||
(defun my/-eshell-prompt-format-span (span)
|
||||
"Format SPAN as \"XhXms.\""
|
||||
(let* ((hours (/ span 3600))
|
||||
(mins (% (/ span 60) 60))
|
||||
(secs (% span 60)))
|
||||
(concat (unless (= hours 0)
|
||||
(format "%dh" hours))
|
||||
(unless (= mins 0)
|
||||
(format "%dm" mins))
|
||||
(format "%ds" secs))))
|
||||
(defun my/-eshell-prompt-last-command-time (end-time)
|
||||
"Return the prompt component for the time of the last command."
|
||||
(if-let ((my/-eshell-prompt-last-start-time)
|
||||
(len (time-subtract end-time
|
||||
my/-eshell-prompt-last-start-time))
|
||||
(float-len (float-time len))
|
||||
((< 3 float-len))
|
||||
(int-len (round float-len)))
|
||||
(concat " time "
|
||||
(propertize (my/-eshell-prompt-format-span int-len)
|
||||
'face '(:foreground "gold1")))))
|
||||
(defun my/-eshell-prompt-function ()
|
||||
"Function for `eshell-prompt-function'"
|
||||
(let* ((end-time (current-time))
|
||||
(dir (my/-eshell-prompt-get-dir))
|
||||
(prompt (concat
|
||||
"\n"
|
||||
(propertize dir 'face '(:foreground "dark turquoise"))
|
||||
(unless (file-writable-p dir)
|
||||
" ")
|
||||
(my/-eshell-prompt-vc-status)
|
||||
(my/-eshell-prompt-last-command-time end-time)
|
||||
(propertize "\n" 'read-only t 'rear-nonsticky t)
|
||||
(propertize
|
||||
"❯ " 'face `(:foreground
|
||||
,(if (= eshell-last-command-status 0)
|
||||
"lime green"
|
||||
"red"))
|
||||
'rear-nonsticky t))))
|
||||
(setq my/-eshell-prompt-last-start-time nil)
|
||||
prompt))
|
||||
(setq eshell-prompt-function #'my/-eshell-prompt-function
|
||||
eshell-prompt-regexp "^❯ "
|
||||
eshell-highlight-prompt nil))
|
||||
(eshell-print (string-join (bookmark-all-names) " ")))))
|
||||
(use-package esh-help
|
||||
:hook (eshell-mode . my/-setup-eshell-help-func)
|
||||
:init
|
||||
@ -1102,6 +934,10 @@ If no name is given, list all bookmarks instead."
|
||||
(use-package eshell-syntax-highlighting
|
||||
:init
|
||||
(eshell-syntax-highlighting-global-mode 1))
|
||||
(use-package eshell-starship
|
||||
:ensure nil
|
||||
:demand t
|
||||
:hook (eshell-prompt-mode . eshell-starship-prompt-mode))
|
||||
|
||||
;; proced
|
||||
(use-package proced
|
||||
|
Loading…
Reference in New Issue
Block a user