diff --git a/elisp/eshell-starship.el b/elisp/eshell-starship.el index 515bc8a..30ddc4a 100644 --- a/elisp/eshell-starship.el +++ b/elisp/eshell-starship.el @@ -7,15 +7,6 @@ (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"))) @@ -26,7 +17,7 @@ FLAGS are passed to `vc-git-program' as its arguments." (concat "~/" (seq-subseq path (length home))) path)))) -(defun eshell-starship--prompt-cut-path (num path) +(defun eshell-starship--limit-path-parts (num path) "Cut PATH down to NUM components. Example: /this/is/a/path 3-> is/a/path" @@ -38,89 +29,107 @@ Example: "/") (string-join (last parts num) "/")))) -(defun eshell-starship--prompt-get-dir () +(defun eshell-starship--get-current-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)))) + (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--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))) +(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 - ((and ahead behind) ?󰹺) - (ahead ?󰜷) - (behind ?󰜮))))) + ((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))) -(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--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)) + ?+))) -(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--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--prompt-git-get-operation () - "Return the current git operation. For example, a revert." +(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)) + (while (not (= (pos-bol) (pos-eol))) + (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." (let ((git-dir (expand-file-name ".git" (vc-git-root default-directory)))) (cond ((file-exists-p (expand-file-name "rebase-apply/applying" git-dir)) @@ -140,36 +149,48 @@ STATUS-LINE is the first line of output from \"git status --porcelain=v1 -b\"." ((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--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--prompt-vc-status () +(defun eshell-starship--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) + (eshell-starship--git-status) (propertize (concat "  " (downcase (symbol-name backend))) 'face '(:foreground "purple"))))) -(defvar-local eshell-starship--prompt-last-start-time nil +(defvar-local eshell-starship--last-start-time nil "Start time of last eshell command.") -(defun eshell-starship--prompt-timer-pre-cmd () +(defun eshell-starship--timer-pre-cmd () "Command run before each eshell program to record the time." - (setq eshell-starship--prompt-last-start-time (current-time))) + (setq eshell-starship--last-start-time (current-time))) -(add-hook 'eshell-pre-command-hook #'eshell-starship--prompt-timer-pre-cmd) +(add-hook 'eshell-pre-command-hook #'eshell-starship--timer-pre-cmd) (defun eshell-starship--prompt-format-span (span) "Format SPAN as \"XhXms\"." @@ -182,12 +203,12 @@ STATUS-LINE is the first line of output from \"git status --porcelain=v1 -b\"." (format "%dm" mins)) (format "%ds" secs)))) -(defun eshell-starship--prompt-last-command-time (end-time) +(defun eshell-starship--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) + (if-let ((eshell-starship--last-start-time) (len (time-subtract end-time - eshell-starship--prompt-last-start-time)) + eshell-starship--last-start-time)) (float-len (float-time len)) ((< 3 float-len)) (int-len (round float-len))) @@ -198,14 +219,14 @@ END-TIME is the time when the command finished executing." (defun eshell-starship--prompt-function () "Function for `eshell-prompt-function'." (let* ((end-time (current-time)) - (dir (eshell-starship--prompt-get-dir)) + (dir (eshell-starship--get-current-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) + (eshell-starship--vc-status) + (eshell-starship--last-command-time end-time) (propertize "\n" 'read-only t 'rear-nonsticky t) (propertize "❯ " 'face `(:foreground @@ -213,7 +234,7 @@ END-TIME is the time when the command finished executing." "lime green" "red")) 'rear-nonsticky t)))) - (setq eshell-starship--prompt-last-start-time nil) + (setq eshell-starship--last-start-time nil) prompt)) (defvar-local ehsell-starship--restore-state nil