p11k/p11k.el

361 lines
12 KiB
EmacsLisp
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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