emacs-config/elisp/eshell-starship.el

238 lines
9.7 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)
(require 'cl-lib)
(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-current-branch-status (status-line)
"Get the status char for the current branch and its remote.
STATUS-LINE is the first line of output from \"git status --porcelain=v1 -b\"."
(when (string-match
"\\[\\(?:ahead \\([0-9]+\\)\\)?,? ?\\(?:behind \\([0-9]+\\)\\)?\\]$"
status-line)
(let ((ahead (match-string 1 status-line))
(behind (match-string 2 status-line)))
(cond
((and ahead behind) ?󰹺)
(ahead ?󰜷)
(behind ?󰜮)))))
(defun eshell-starship--prompt-git-has-stash ()
"Return t if the current git directory has a stash, nil otherwise."
(zerop (process-file vc-git-program nil nil nil
"rev-parse" "--verify" "refs/stash")))
(defun eshell-starship--prompt-git-state-chars ()
"Get chars, like + and ✘ for `eshell-starship--prompt-function'."
(with-temp-buffer
(when (zerop (vc-git-command t nil nil "status" "--porcelain=v1" "-b"))
(goto-char (point-min))
(cl-loop with command-error-function = nil
with status-arr = nil
with first-line = (buffer-substring-no-properties
(point) (pos-eol))
;; account for newline at end
with line-count = (car (buffer-line-statistics))
with cur-buf = (current-buffer)
do (forward-line)
for x_status = (char-after)
for y_status = (char-after (1+ (point)))
until (> (line-number-at-pos) line-count)
do
(cond
((or (= ?D x_status y_status)
(= ?A x_status y_status)
(= ?U x_status y_status)
(and (= ?A x_status) (= ?U y_status))
(and (= ?U x_status) (= ?D y_status))
(and (= ?U x_status) (= ?A y_status))
(and (= ?D x_status) (= ?U y_status)))
(push ?= status-arr))
((or (= x_status ?D) (= y_status ?D))
(push ? status-arr))
((or (= x_status ?R) (= y_status ?R))
(push status-arr))
((= y_status ?M)
(push ?! status-arr))
((or (= x_status ?A) (= x_status ?M))
(push ?+ status-arr))
((= x_status y_status ??)
(push ?? status-arr)))
finally
(sort status-arr #'(lambda (a b)
(cond
((= a ?=)
t)
((= b ?=)
nil)
(t
(< a b)))))
(when (eshell-starship--prompt-git-has-stash)
(if (= (car status-arr) ?=)
(setq status-arr (append '(?= ?$) (cdr status-arr)))
(push ?$ status-arr)))
(when-let (branch-status (eshell-starship--prompt-current-branch-status
first-line))
(push branch-status status-arr))
finally return (apply 'string (seq-uniq 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 "rebase-apply/applying" git-dir))
"AM")
((file-exists-p (expand-file-name "rebase-apply/rebasing" git-dir))
"REBASE")
((file-exists-p (expand-file-name "rebase-apply" git-dir))
"AM/REBASE")
((file-exists-p (expand-file-name "rebase-merge" git-dir))
"REBASING")
((file-exists-p (expand-file-name "CHERRY_PICK_HEAD" git-dir))
"CHERRY-PICKING")
((file-exists-p (expand-file-name "MERGE_HEAD" git-dir))
"MERGING")
((file-exists-p (expand-file-name "BISECT_LOG" git-dir))
"BISECTING")
((file-exists-p (expand-file-name "REVERT_HEAD" git-dir))
"REVERTING"))))
(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