;;; 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." (= (process-file vc-git-program nil nil nil "rev-parse" "--verify" "refs/stash") 0)) (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" "-b")) (branch-status (eshell-starship--prompt-current-branch-status (car lines))) (status-arr)) (dolist (line (cdr 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)) (push ?! status-arr)) ((= status-char ??) (push ?? status-arr)) ((or (= status-char ?A) (= status-char ?C)) (push ?+ status-arr)) ((= status-char ?D) (push ? status-arr)) ((= status-char ?R) (push ?» status-arr)) ((= status-char ?U) (push ?= status-arr))))) (when (eshell-starship--prompt-git-has-stash) (push ?$ status-arr)) (sort status-arr #'<) (when branch-status (push branch-status status-arr)) (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 "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