emacs-config/elisp/eshell-starship.el

259 lines
10 KiB
EmacsLisp
Raw Normal View History

2024-01-18 23:37:05 -08:00
;;; eshell-starship.el --- Starship-like (https://starship.rs) prompt for eshell -*- lexical-binding: t; -*-
2024-01-17 04:16:49 -08:00
;;; Commentary:
;;; Code:
(require 'vc)
(require 'vc-git)
(require 'eshell)
(require 'cl-lib)
2024-01-17 04:16:49 -08:00
(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))))
2024-01-18 16:11:13 -08:00
(defun eshell-starship--limit-path-parts (num path)
2024-01-17 04:16:49 -08:00
"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) "/"))))
2024-01-18 16:11:13 -08:00
(defun eshell-starship--get-current-dir ()
2024-01-17 04:16:49 -08:00
"Get dir for `eshell-starship--prompt-function'."
2024-01-18 16:11:13 -08:00
(eshell-starship--limit-path-parts
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--git-parse-status-headers ()
"Parse the status headers (read from the current buffer).
The headers are as described in the porcelain v2 section of the git-status(3)
man page.
The return value is a list of the form (oid head upstream ahead behind stash)"
(let ((oid nil)
(head nil)
(upstream nil)
(ahead nil)
(behind nil)
(stash nil))
(while (and (char-after) (= (char-after) ?#))
(forward-char 2)
(cond
2024-01-18 16:11:13 -08:00
((looking-at "branch\\.oid ")
(setq oid (buffer-substring-no-properties
(match-end 0)
(pos-eol))))
((looking-at "branch\\.head ")
(setq head (buffer-substring-no-properties
(match-end 0)
(pos-eol))))
((looking-at "branch\\.upstream ")
(setq upstream (buffer-substring-no-properties
(match-end 0)
(pos-eol))))
((looking-at "branch\\.ab ")
(let ((ab-str (buffer-substring-no-properties
(match-end 0)
(pos-eol))))
(when (string-match "\\(+[0-9]+\\) \\(-[0-9]+\\)$"
ab-str)
(setq ahead (string-to-number (match-string 1 ab-str))
behind (string-to-number (match-string 2 ab-str))))))
((looking-at "stash ")
(setq stash (string-to-number (buffer-substring-no-properties
(match-end 0)
(pos-eol))))))
(forward-line))
(list oid head upstream (or ahead 0) (or behind 0) stash)))
2024-01-18 16:11:13 -08:00
(defun eshell-starship--git-interpret-file-status (x y)
"Return the prompt character for the status X and Y.
A description of X and Y can be found in the git-status(3) man page."
(cond
((or (= x ?D) (= y ?D))
?)
((or (= x ?R) (= y ?R))
)
((= y ?M)
?!)
((or (= x ?A) (= x ?M))
?+)))
2024-01-17 04:16:49 -08:00
2024-01-18 16:11:13 -08:00
(defun eshell-starship--git-interpret-branch-status (ahead behind)
"Get the status char for the current branch and its remote.
AHEAD should evaluate to t if the current branch is ahead of its remote, and
BEHIND should evaluate to t if the current branch is behind its remote."
(cond
((and ahead behind) "󰹺")
(ahead "󰜷")
(behind "󰜮")))
(defun eshell-starship--git-file-status (stash ahead behind)
"Get the file status string for the git prompt module.
STASH should be t if there is current stashed data stash. AHEAD and BEHIND
should be as for `eshell-starship--git-interpret-branch-status'."
(let ((merge-conflicts nil)
(status-chars nil))
2024-01-18 23:37:05 -08:00
(while (not (eobp))
2024-01-18 16:11:13 -08:00
(cond
((= (char-after) ??)
(push ?? status-chars))
((= (char-after) ?u)
(setq merge-conflicts t))
((or (= (char-after) ?1)
(= (char-after) ?2))
(push (eshell-starship--git-interpret-file-status
(char-after (+ 2 (point)))
(char-after (+ 3 (point))))
status-chars)))
(forward-line))
(concat (eshell-starship--git-interpret-branch-status (not (zerop ahead))
(not (zerop behind)))
(when merge-conflicts "=")
(when stash "$")
(apply 'string (sort (seq-uniq status-chars) #'<)))))
(defun eshell-starship--git-current-operation ()
"Return the current git operation.
For example, a revert. If there is no current operation, return nil."
2024-01-17 04:16:49 -08:00
(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
2024-01-18 16:11:13 -08:00
(defun eshell-starship--git-status ()
"Return the text for the git module for `eshell-starship--prompt-function'."
(with-temp-buffer
(when (zerop (vc-git-command t nil nil "status" "--porcelain=v2"
"--branch" "--show-stash"))
(goto-char (point-min))
(cl-destructuring-bind (oid head upstream ahead behind stash)
(eshell-starship--git-parse-status-headers)
(let ((file-status (eshell-starship--git-file-status stash ahead
behind))
(operation (eshell-starship--git-current-operation)))
(concat
(if (string= "(detached)" head)
(propertize (concat " (" (substring oid 0 7) ")")
'face '(:foreground "lawn green"))
(propertize (concat " 󰊢 " head)
'face '(:foreground "medium purple")))
(unless (string-empty-p file-status)
(propertize (concat " [" file-status "]")
'face '(:foreground "red")))
(when operation
(concat " (" (propertize
operation 'face
'(:inherit bold :foreground "yellow")) ")"))))))))
(defun eshell-starship--vc-status ()
2024-01-17 04:16:49 -08:00
"Get vc status for `eshell-starship--prompt-function'."
(if-let (backend (vc-responsible-backend default-directory t))
(if (eq backend 'Git)
2024-01-18 16:11:13 -08:00
(eshell-starship--git-status)
2024-01-17 04:16:49 -08:00
(propertize
(concat "" (downcase (symbol-name backend)))
'face '(:foreground "purple")))))
2024-01-18 16:11:13 -08:00
(defvar-local eshell-starship--last-start-time nil
2024-01-17 04:16:49 -08:00
"Start time of last eshell command.")
2024-01-18 16:11:13 -08:00
(defun eshell-starship--timer-pre-cmd ()
2024-01-17 04:16:49 -08:00
"Command run before each eshell program to record the time."
2024-01-18 16:11:13 -08:00
(setq eshell-starship--last-start-time (current-time)))
2024-01-17 04:16:49 -08:00
2024-01-18 16:11:13 -08:00
(add-hook 'eshell-pre-command-hook #'eshell-starship--timer-pre-cmd)
2024-01-17 04:16:49 -08:00
(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))))
2024-01-18 16:11:13 -08:00
(defun eshell-starship--last-command-time (end-time)
2024-01-17 04:16:49 -08:00
"Return the prompt component for the time of the last command.
END-TIME is the time when the command finished executing."
2024-01-18 16:11:13 -08:00
(if-let ((eshell-starship--last-start-time)
2024-01-17 04:16:49 -08:00
(len (time-subtract end-time
2024-01-18 16:11:13 -08:00
eshell-starship--last-start-time))
2024-01-17 04:16:49 -08:00
(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))
2024-01-18 16:11:13 -08:00
(dir (eshell-starship--get-current-dir))
2024-01-17 04:16:49 -08:00
(prompt (concat
"\n"
(propertize dir 'face '(:foreground "dark turquoise"))
(unless (file-writable-p dir)
"")
2024-01-18 16:11:13 -08:00
(eshell-starship--vc-status)
(eshell-starship--last-command-time end-time)
2024-01-17 04:16:49 -08:00
(propertize "\n" 'read-only t 'rear-nonsticky t)
(propertize
" " 'face `(:foreground
,(if (= eshell-last-command-status 0)
"lime green"
"red"))
'rear-nonsticky t))))
2024-01-18 16:11:13 -08:00
(setq eshell-starship--last-start-time nil)
2024-01-17 04:16:49 -08:00
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