emacs-config/elisp/eshell-starship.el

238 lines
9.7 KiB
EmacsLisp
Raw Normal View History

2024-01-17 04:16:49 -08:00
;;; eshell-starship.el --- Starship-like (https://starship.rs) prompt for eshell
;;; Commentary:
;;; Code:
(require 'vc)
(require 'vc-git)
(require 'eshell)
(require 'cl-lib)
2024-01-17 04:16:49 -08:00
(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."
2024-01-18 03:14:00 -08:00
(zerop (process-file vc-git-program nil nil nil
"rev-parse" "--verify" "refs/stash")))
2024-01-17 04:16:49 -08:00
(defun eshell-starship--prompt-git-state-chars ()
"Get chars, like + and ✘ for `eshell-starship--prompt-function'."
2024-01-18 03:14:00 -08:00
(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)
2024-01-17 04:16:49 -08:00
do
2024-01-18 03:14:00 -08:00
(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))))))
2024-01-17 04:16:49 -08:00
(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
2024-01-18 03:14:00 -08:00
((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")
2024-01-17 04:16:49 -08:00
((file-exists-p (expand-file-name "rebase-merge" git-dir))
"REBASING")
2024-01-18 03:14:00 -08:00
((file-exists-p (expand-file-name "CHERRY_PICK_HEAD" git-dir))
"CHERRY-PICKING")
2024-01-17 04:16:49 -08:00
((file-exists-p (expand-file-name "MERGE_HEAD" git-dir))
2024-01-18 03:14:00 -08:00
"MERGING")
((file-exists-p (expand-file-name "BISECT_LOG" git-dir))
"BISECTING")
((file-exists-p (expand-file-name "REVERT_HEAD" git-dir))
"REVERTING"))))
2024-01-17 04:16:49 -08:00
(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)."
2024-01-17 04:16:49 -08:00
: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