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