emacs-config/elisp/eshell-starship.el

215 lines
8.5 KiB
EmacsLisp
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; 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