361 lines
12 KiB
EmacsLisp
361 lines
12 KiB
EmacsLisp
;;; p11k.el --- PowerLevel10k prompt for Eshell -*- lexical-binding: t; -*-
|
||
|
||
;; Copyright (C) 2023 Mitchell Marquez
|
||
|
||
;; Author: Mitchell Marquez <dr.m.perseos@gmail.com>
|
||
;; Keywords: terminals, convenience, faces
|
||
;; Package-Requires: ((dash) (all-the-icons) (eshell-prompt-extras) (f) (shrink-path))
|
||
|
||
;;; Commentary:
|
||
;; PowerLevel10k prompt theme for Eshell. Tries to emulate everything but may
|
||
;; be broken.
|
||
|
||
;;; Bugs:
|
||
;; - styles other than Lean not implemented
|
||
;; - right-aligned elements (time display) get pushed to new line when `word-wrap' t
|
||
|
||
;;; Code:
|
||
(require 'f)
|
||
(require 'dash)
|
||
(require 'eshell-prompt-extras)
|
||
(require 'shrink-path)
|
||
|
||
(defvar p11k/eol-marker "") ;; zero width non-joiner, 8204
|
||
|
||
(defgroup p11k nil
|
||
"PowerLevel11k Prompt for Eshell."
|
||
:group 'eshell
|
||
:prefix "p11k/")
|
||
|
||
(defcustom p11k/use-transient-prompt t
|
||
"Whether to enable `transient-prompt' for p11k."
|
||
:group 'p11k
|
||
:type 'boolean) ;; or nil to disable transient-prompt
|
||
|
||
(defcustom p11k/prompt-style 'lean
|
||
"Style of PowerLevel11k Eshell prompt."
|
||
:group 'p11k
|
||
:type '(choice
|
||
(const :tag "Lean" lean)
|
||
(const :tag "Classic" classic)
|
||
(const :tag "Rainbow" rainbow)
|
||
(const :tag "Pure" pure)))
|
||
(defcustom p11k/use-unicode nil
|
||
"Whether or not to use Unicode characters in PowerLevel11k Eshell prompt."
|
||
:group 'p11k
|
||
:type 'boolean)
|
||
(defcustom p11k/prompt-color-level 2
|
||
"Darkness of the prompt background for PowerLevel11k."
|
||
:group 'p11k
|
||
:type '(choice
|
||
(const :tag "Lightest" 1)
|
||
(const :tag "Light" 2)
|
||
(const :tag "Dark" 3)
|
||
(const :tag "darkest" 4)))
|
||
(defcustom p11k/show-current-time 12
|
||
"Format of time display, or nil for no time."
|
||
:group 'p11k
|
||
:type '(choice
|
||
(const :tag "No" nil)
|
||
(const :tag "24-hour format" 24)
|
||
(const :tag "12-hour format" 12)))
|
||
(defcustom p11k/prompt-segment-separator-type 'angled
|
||
"Type of separator between p11k prompt segments."
|
||
:group 'p11k
|
||
:type '(choice
|
||
(const :tag "Angled" angled)
|
||
(const :tag "Vertical" vertical)
|
||
(const :tag "Slanted" slanted)
|
||
(const :tag "Round" round))) ;; Only when unicode=t (?)
|
||
(defcustom p11k/prompt-segment-head-type 'slanted
|
||
"Type of head character to put on the inner ends of p11k prompt."
|
||
:group 'p11k
|
||
:type '(choice
|
||
(const :tag "Sharp" sharp)
|
||
(const :tag "Blurred" blurred)
|
||
(const :tag "Slanted" slanted)
|
||
(const :tag "Round" round))) ;; Only when unicode=t (?)
|
||
(defcustom p11k/prompt-segment-tail-type 'slanted
|
||
"Type of tail character to put on the outer ends of p11k prompt."
|
||
:group 'p11k
|
||
:type '(choice
|
||
(const :tag "Flat" flat)
|
||
(const :tag "Blurred" blurred)
|
||
(const :tag "Sharp" sharp)
|
||
(const :tag "Slanted" slanted)
|
||
(const :tag "Round" round))) ;; Only when unicode=t (?)
|
||
(defcustom p11k/prompt-height 2
|
||
"Whether to use one or two lines for p11k prompt."
|
||
:group 'p11k
|
||
:type '(choice
|
||
(const :tag "One line" 1)
|
||
(const :tag "Two Lines" 2)))
|
||
(defcustom p11k/prompt-segment-connection 'disconnected
|
||
"What style of connector, if any, to draw between p11k prompt segments."
|
||
:group 'p11k
|
||
:type '(choice
|
||
(const :tag "Disconnected" disconnected)
|
||
(const :tag "Dotted" dotted)
|
||
(const :tag "Solid" solid)))
|
||
(defcustom p11k/prompt-frame 'full
|
||
"Which side of the window to put a frame between p11k prompt segments."
|
||
:group 'p11k
|
||
:type '(choice
|
||
(const :tag "No frame" nil)
|
||
(const :tag "Left" left)
|
||
(const :tag "Right" right)
|
||
(const :tag "Full" full)))
|
||
(defcustom p11k/prompt-spacing 'compact
|
||
"How much distance to put between consecutive prompts and command outputs."
|
||
:group 'p11k
|
||
:type '(choice
|
||
(const :tag "Compact" compact)
|
||
(const :tag "Sparse" sparse)))
|
||
(defcustom p11k/use-icons nil
|
||
"Whether to use icons in the p11k prompt."
|
||
:group 'p11k
|
||
:type 'boolean) ;; or t for "many" icons
|
||
(defcustom p11k/prompt-flow 'concise
|
||
"Whether to put prepositions and adjectives around p11k segments."
|
||
:group 'p11k
|
||
:type '(choice
|
||
(const :tag "Concise (fewer words)" concise)
|
||
(const :tag "Fluent (more words)" fluent)))
|
||
(defvar p11k/prompt-caret (if p11k/use-unicode
|
||
"❯" ">"))
|
||
(defvar p11k/prompt-caret-alist
|
||
(if p11k/use-unicode
|
||
'((insert . "❯")
|
||
(normal . "❮")
|
||
(visual . "") ;from nerd-icons
|
||
)
|
||
'((insert . ">")
|
||
(normal . "<")
|
||
(visual . "V"))))
|
||
(defvar p11k/unwritable-icon (if p11k/use-unicode
|
||
"" "!w"))
|
||
(defvar p11k/last-prompt-bound 0) ;; internal variable
|
||
|
||
(defface p11k/green '((t (:foreground "green")))
|
||
"Face for \"green\" parts of p11k prompt and git status segment.")
|
||
(defface p11k/yellow '((t (:foreground "yellow")))
|
||
"Face for \"yellow\" parts of p11k prompt and git status segment.")
|
||
(defface p11k/blue '((t (:foreground "blue")))
|
||
"Face for \"blue\" parts of p11k prompt and git status segment.")
|
||
(defface p11k/red '((t (:foreground "red")))
|
||
"Face for \"red\" parts of p11k prompt and git status segment.")
|
||
(defface p11k/cyan '((t (:foreground "cyan")))
|
||
"Face for \"cyan\" parts of p11k prompt.")
|
||
|
||
(defun p11k/setup-git-status ()
|
||
"Declare variables and faces for git info in p11k prompt."
|
||
(face-spec-set 'gitstatus-clean-face '((t (:inherit 'p11k/green))))
|
||
(face-spec-set 'gitstatus-modified-face '((t (:inherit 'p11k/yellow))))
|
||
(face-spec-set 'gitstatus-untracked-face '((t (:inherit 'p11k/blue)))))
|
||
(defun p11k/spaced-concat (&rest strings)
|
||
(string-trim-left
|
||
(mapconcat
|
||
(lambda (str) (format " %s" str))
|
||
strings)))
|
||
;; (p11k/spaced-concat "one" "two" (propertize "five" 'face 'p11k/yellow))
|
||
|
||
(defun p11k/format-directory (dir &optional maxwidth)
|
||
"Format DIR for use in p11k Eshell prompt. Returneth a string.
|
||
|
||
if MAXWIDTH is non-nil, abbreviate."
|
||
(let* ((writable-symbol (if (file-writable-p dir) "" p11k/unwritable-icon))
|
||
(dir (string-replace (getenv "HOME") "~" dir)))
|
||
(p11k/spaced-concat
|
||
writable-symbol
|
||
(if (or maxwidth (>= (* 2 (length dir)) (window-text-width)))
|
||
(shrink-path-dirs dir) dir))))
|
||
|
||
(defun p11k/caret-color-status (status)
|
||
"Return the propertized `p11k/prompt-caret', according to STATUS."
|
||
(let ((facecolor (if (zerop status) 'p11k/green 'p11k/red)))
|
||
(propertize (format "%s" (alist-get (p11k/get-modal-status) p11k/prompt-caret-alist))
|
||
'face facecolor)))
|
||
(defun p11k/get-modal-status ()
|
||
(if (bound-and-true-p meow--current-state)
|
||
meow--current-state
|
||
evil-state))
|
||
|
||
(defvar p11k/sep-str-alist
|
||
'((angled . ("" . ""))
|
||
(sharp . ("" . ""))
|
||
(vertical . ("" . ""))
|
||
(flat . ("" . ""))
|
||
(slanted . ("" . ""))
|
||
(round . ("" . ""))
|
||
(blurred . ("▓▒░" . "░▒▓")))
|
||
"Alist of separator names and their canonical string appearances.
|
||
|
||
Each string appearance is a cons cell corresponding with the left-handed form as
|
||
the car and right-handed form as the cdr.")
|
||
|
||
(defun p11k/get-sep-char (sym &optional direction)
|
||
"Given symbol SYM name of a prompt style, return matching string.
|
||
|
||
Corresponds with DIRECTION in only a specific idealized context. If DIRECTION
|
||
is omitted, just return the cons of bolth."
|
||
(let ((symcons (alist-get sym p11k/sep-str-alist))
|
||
(getter (if (eq direction 'left) #'car #'cdr)))
|
||
(if direction
|
||
(funcall getter symcons)
|
||
symcons)))
|
||
|
||
(defun p11k/status-formatter (timestamp duration)
|
||
"Return status display for `epe-status'. Replaceth `epe-status-formatter'.
|
||
|
||
TIMESTAMP is the value returned by `current-time' and DURATION is the floating
|
||
time the command took to complete in seconds.
|
||
|
||
Outputteth a string."
|
||
;; TIMESTAMP is irrelevant for us
|
||
(let ((durstring (format "%.3fs" duration)))
|
||
(p11k/spaced-concat
|
||
(if (eq p11k/prompt-flow 'fluent) "took" "")
|
||
(propertize durstring 'face 'p11k/yellow))))
|
||
|
||
(defun p11k/justificate (left-items right-items)
|
||
"Concatenate LEFT-ITEMS and RIGHT-ITEMS with a gap between.
|
||
Use 'window-width' to calculate the total final width."
|
||
(let* ((left-str (eval `(p11k/spaced-concat ,@left-items)))
|
||
(right-str (eval `(p11k/spaced-concat ,@right-items)))
|
||
(left-length (length left-str))
|
||
(right-length (length right-str))
|
||
(middle-spc (- (window-width nil 'remap) (+ left-length right-length))))
|
||
(concat left-str (make-string middle-spc (string-to-char " ")) right-str)))
|
||
;; (eval `(concat ,@'("one" "two")))
|
||
;; (p11k/justificate '("one" "wto" "three") '("twenty" "23" "forthy-three"))
|
||
|
||
(defun p11k/git-status ()
|
||
"Return propertized git information."
|
||
(if (file-exists-p ".git")
|
||
(let* ((status-alist
|
||
(--> "git status -s"
|
||
(shell-command-to-string it)
|
||
(split-string it "\n" 'omit-nulls search-whitespace-regexp)
|
||
(--map (let ((parts (split-string it)))
|
||
(cons (car parts) (cadr parts)))
|
||
it)))
|
||
(branch (--> "git branch"
|
||
(shell-command-to-string it)
|
||
(split-string it "\n" 'omit-nulls search-whitespace-regexp)
|
||
(--filter (string-match-p (rx bol "* " (+ word)) it) it)
|
||
(split-string (car it))
|
||
(cadr it)))
|
||
(modcount (--> status-alist
|
||
(--map (car it) it)
|
||
(--count (string-match-p "M" it) it)
|
||
(if (not (= it 0))
|
||
(format "!%s" it)
|
||
nil)))
|
||
(untracked (--> status-alist
|
||
(--map (car it) it)
|
||
(--count (string-match-p "\\?\\?" it) it)
|
||
(if (not (= it 0))
|
||
(format "?%s" it)
|
||
nil))))
|
||
(p11k/spaced-concat
|
||
(if (eq p11k/prompt-flow 'fluent) "on" "")
|
||
(propertize branch 'face 'p11k/green)
|
||
(if modcount (propertize modcount 'face 'p11k/yellow) "")
|
||
(if untracked (propertize untracked 'face 'p11k/blue) "")))
|
||
""))
|
||
|
||
(defun p11k/prompt ()
|
||
"Return the propertized face for p11k Eshell prompt."
|
||
(let ((sparsity (if (eq p11k/prompt-spacing 'sparse) "\n" ""))
|
||
(directory (propertize (format "%s" (p11k/format-directory default-directory))
|
||
'face 'p11k/blue))
|
||
(timedisp
|
||
(if p11k/show-current-time
|
||
(p11k/spaced-concat
|
||
(if (eq p11k/prompt-style 'fluent) "at" "")
|
||
(propertize (pcase p11k/show-current-time
|
||
(12 (format-time-string "%I:%M:%S %p"))
|
||
(24 (format-time-string "%H:%M:%S"))
|
||
(t "[bad time type]"))
|
||
'face 'p11k/cyan))
|
||
""))
|
||
(status-str (epe-status #'p11k/status-formatter)))
|
||
(p11k/justificate
|
||
`(,sparsity
|
||
,directory
|
||
,(p11k/git-status))
|
||
;; justificate after this
|
||
`(,(concat p11k/eol-marker
|
||
(p11k/spaced-concat
|
||
status-str
|
||
timedisp)
|
||
"\n"
|
||
(p11k/caret-color-status eshell-last-command-status)
|
||
" ")))))
|
||
|
||
(defun p11k/modal-caret (&optional throwaway)
|
||
"Toggle the direction that the prompt points according to `p11k/get-modal-status'."
|
||
(save-match-data
|
||
(save-mark-and-excursion
|
||
(let ((inhibit-read-only t)
|
||
(new-caret
|
||
(p11k/caret-color-status
|
||
eshell-last-command-status)))
|
||
(goto-char (point-max))
|
||
(re-search-backward eshell-prompt-regexp)
|
||
(goto-char (point-at-bol))
|
||
(delete-forward-char 1)
|
||
(insert new-caret))
|
||
)))
|
||
|
||
(defun p11k/esh-transient-prompt ()
|
||
"Delete the first line of a multi-line eshell prompt.
|
||
|
||
To be run before every eshell command."
|
||
(if p11k/use-transient-prompt
|
||
(save-match-data
|
||
(save-excursion
|
||
(setq p11k/last-prompt-bound
|
||
(or (ignore-errors (search-backward p11k/eol-marker nil t))
|
||
(point-min)))
|
||
(let ((inhibit-read-only t))
|
||
(delete-line))))))
|
||
|
||
(defun p11k/disable ()
|
||
"Turn off p11k and restore the old prompt."
|
||
(remove-hook 'eshell-pre-command-hook #'p11k/esh-transient-prompt)
|
||
(remove-hook 'eshell-mode-hook #'p11k/setup-git-status)
|
||
(setq-default eshell-prompt-function p11k/old-eshell-prompt-function)
|
||
(setq-default eshell-prompt-regexp p11k/old-eshell-prompt-regexp))
|
||
|
||
;;;###autoload
|
||
(progn
|
||
(defun p11k/init ()
|
||
"Run setup functions for p11k."
|
||
(require 'p11k)
|
||
(if p11k/use-transient-prompt
|
||
(add-hook 'eshell-pre-command-hook #'p11k/esh-transient-prompt))
|
||
(p11k/setup-git-status)
|
||
(defvar p11k/old-eshell-prompt-function eshell-prompt-function)
|
||
(defvar p11k/old-eshell-prompt-regexp eshell-prompt-regexp)
|
||
(setq eshell-prompt-function #'p11k/prompt)
|
||
(if (featurep 'meow)
|
||
(add-hook 'meow-switch-state-hook #'p11k/modal-caret nil 'local)
|
||
(if (featurep 'evil)
|
||
(progn
|
||
(add-hook 'evil-insert-state-entry-hook #'p11k/modal-caret nil 'local)
|
||
(add-hook 'evil-normal-state-entry-hook #'p11k/modal-caret nil 'local)
|
||
(add-hook 'evil-visual-state-entry-hook #'p11k/modal-caret nil 'local))))
|
||
(setq eshell-prompt-regexp
|
||
(rx bol
|
||
(regexp (eval `(rx ,(cons 'or (mapcar #'cdr p11k/prompt-caret-alist)))))
|
||
" ")))
|
||
(define-minor-mode p11k-mode
|
||
"PowerLevel11k Prompt for Eshell."
|
||
:lighter " p11k"
|
||
:require 'p11k
|
||
(p11k/init)))
|
||
|
||
(provide 'p11k)
|
||
;;; p11k.el ends here
|