0 | 0 |
;;; eshell-prompt-extras.el --- Display extra information for your eshell prompt.
|
1 | 1 |
|
2 | |
;; Copyright (C) 2014-2016 Wei Zhao
|
3 | |
|
4 | |
;; Author: Wei Zhao <kaihaosw@gmail.com>
|
|
2 |
;; Copyright (C) 2014-2019 Wei Zhao
|
|
3 |
|
|
4 |
;; Author: zwild <judezhao@outlook.com>
|
5 | 5 |
;; Contributors: Lee Hinman
|
6 | 6 |
;; Maintainer: Chunyang Xu <mail@xuchunyang.me>
|
7 | |
;; URL: https://github.com/hiddenlotus/eshell-prompt-extras
|
8 | |
;; Version: 0.96
|
|
7 |
;; URL: https://github.com/zwild/eshell-prompt-extras
|
|
8 |
;; Version: 1.0
|
9 | 9 |
;; Created: 2014-08-16
|
10 | 10 |
;; Keywords: eshell, prompt
|
|
11 |
;; Package-Requires: ((emacs "25"))
|
11 | 12 |
|
12 | 13 |
;; This file is NOT part of GNU Emacs.
|
13 | 14 |
|
|
35 | 36 |
;; number for eshell prompt.
|
36 | 37 |
|
37 | 38 |
;; If you want to display the python virtual environment info, you
|
38 | |
;; need to install `virtualenvwrapper' and `virtualenvwrapper.el'.
|
39 | |
;; pip install virtualenvwrapper
|
|
39 |
;; need to install `virtualenvwrapper.el'.
|
40 | 40 |
;; M-x: package-install: virtualenvwrapper
|
41 | 41 |
|
42 | 42 |
;; Installation
|
|
80 | 80 |
(require 'em-dirs)
|
81 | 81 |
(require 'esh-ext)
|
82 | 82 |
(require 'tramp)
|
|
83 |
(require 'subr-x)
|
|
84 |
(require 'seq)
|
83 | 85 |
(autoload 'cl-reduce "cl-lib")
|
84 | 86 |
(autoload 'vc-git-branches "vc-git")
|
85 | 87 |
(autoload 'vc-find-root "vc-hooks")
|
86 | 88 |
|
87 | |
(when (require 'virtualenvwrapper nil t)
|
88 | |
(defun epe-venv-p ()
|
89 | |
"If you are `workon'ing some virtual environment."
|
90 | |
(and (eshell-search-path "virtualenvwrapper.sh")
|
91 | |
(string-match venv-location (eshell-search-path "python")))))
|
92 | |
|
93 | 89 |
(defgroup epe nil
|
94 | 90 |
"Eshell extras"
|
95 | 91 |
:group 'eshell-prompt)
|
|
120 | 116 |
:type '(choice (const :tag "fish-style-dir-name" fish)
|
121 | 117 |
(const :tag "single-dir-name" single)
|
122 | 118 |
(const :tag "full-path-name" full)))
|
|
119 |
|
|
120 |
(defcustom epe-fish-path-max-len 30
|
|
121 |
"Default maximum length for path in `epe-fish-path'."
|
|
122 |
:group 'epe
|
|
123 |
:type 'number)
|
123 | 124 |
|
124 | 125 |
(defface epe-remote-face
|
125 | 126 |
'((t (:inherit font-lock-comment-face)))
|
|
138 | 139 |
"Face of directory in prompt."
|
139 | 140 |
:group 'epe)
|
140 | 141 |
|
|
142 |
(defface epe-git-dir-face
|
|
143 |
`((t (:foreground "gold")))
|
|
144 |
"Face of git path component in prompt."
|
|
145 |
:group 'epe)
|
|
146 |
|
141 | 147 |
(defface epe-git-face
|
142 | 148 |
'((t (:inherit font-lock-constant-face)))
|
143 | 149 |
"Face of git info in prompt."
|
|
173 | 179 |
(defface epe-pipeline-time-face
|
174 | 180 |
'((t :foreground "yellow"))
|
175 | 181 |
"Face for time in pipeline theme."
|
|
182 |
:group 'epe)
|
|
183 |
|
|
184 |
(defface epe-status-face
|
|
185 |
'((t (:inherit font-lock-keyword-face)))
|
|
186 |
"Face of command status line (duration, termination timestamp)."
|
176 | 187 |
:group 'epe)
|
177 | 188 |
|
178 | 189 |
;; help definations
|
|
191 | 202 |
(replace-regexp-in-string "\n$" "" string))
|
192 | 203 |
|
193 | 204 |
;; https://www.emacswiki.org/emacs/EshellPrompt
|
194 | |
(defun epe-fish-path (path)
|
|
205 |
(defun epe-fish-path (path &optional max-len)
|
195 | 206 |
"Return a potentially trimmed-down version of the directory PATH, replacing
|
196 | 207 |
parent directories with their initial characters to try to get the character
|
197 | 208 |
length of PATH (sans directory slashes) down to MAX-LEN."
|
198 | 209 |
(let* ((components (split-string (abbreviate-file-name path) "/"))
|
199 | |
(max-len 30)
|
|
210 |
(max-len (or max-len epe-fish-path-max-len))
|
200 | 211 |
(len (+ (1- (length components))
|
201 | 212 |
(cl-reduce '+ components :key 'length)))
|
202 | 213 |
(str ""))
|
|
216 | 227 |
components (cdr components)))
|
217 | 228 |
(concat str (cl-reduce (lambda (a b) (concat a "/" b)) components))))
|
218 | 229 |
|
|
230 |
(defun epe-extract-git-component (path)
|
|
231 |
"Extract and return the tuple (prefix git-component) from PATH."
|
|
232 |
(let ((prefix path)
|
|
233 |
git-component)
|
|
234 |
(when (epe-git-p)
|
|
235 |
;; We need "--show-prefix and not "--top-level" when we don't follow symlinks.
|
|
236 |
(let* ((git-file-path (abbreviate-file-name
|
|
237 |
(string-trim-right
|
|
238 |
(with-output-to-string
|
|
239 |
(with-current-buffer standard-output
|
|
240 |
(call-process "git" nil t nil
|
|
241 |
"rev-parse"
|
|
242 |
"--show-prefix"))))))
|
|
243 |
(common-folder (car (split-string git-file-path "/"))))
|
|
244 |
(setq prefix (string-join (seq-take-while
|
|
245 |
(lambda (s)
|
|
246 |
(not (string= s common-folder)))
|
|
247 |
(split-string path "/"))
|
|
248 |
"/"))
|
|
249 |
(setq git-component
|
|
250 |
(substring-no-properties path
|
|
251 |
(min (length path) (1+ (length prefix)))))))
|
|
252 |
(list prefix git-component)))
|
|
253 |
|
219 | 254 |
(defun epe-user-name ()
|
220 | 255 |
"User information."
|
221 | 256 |
(if (epe-remote-p)
|
|
226 | 261 |
"Date time information."
|
227 | 262 |
(format-time-string (or format "%Y-%m-%d %H:%M") (current-time)))
|
228 | 263 |
|
|
264 |
(defun epe-status-formatter (timestamp duration)
|
|
265 |
"Return the status display for `epe-status'.
|
|
266 |
TIMESTAMP is the value returned by `current-time' and DURATION is the floating
|
|
267 |
time the command took to complete in seconds."
|
|
268 |
(format "#[STATUS] End time %s, duration %.3fs\n"
|
|
269 |
(format-time-string "%F %T" timestamp)
|
|
270 |
duration))
|
|
271 |
|
|
272 |
(defcustom epe-status-min-duration 1
|
|
273 |
"If a command takes more time than this, display its status with `epe-status'."
|
|
274 |
:group 'epe
|
|
275 |
:type 'number)
|
|
276 |
|
|
277 |
(defvar epe-status--last-command-time nil)
|
|
278 |
(make-variable-buffer-local 'epe-status--last-command-time)
|
|
279 |
|
|
280 |
(defun epe-status--record ()
|
|
281 |
(setq epe-status--last-command-time (current-time)))
|
|
282 |
|
|
283 |
(defun epe-status (&optional formatter min-duration)
|
|
284 |
"Termination timestamp and duration of command.
|
|
285 |
Status is only returned if command duration was longer than
|
|
286 |
MIN-DURATION \(defaults to `epe-status-min-duration'). FORMATTER
|
|
287 |
is a function of two arguments, TIMESTAMP and DURATION, that
|
|
288 |
returns a string."
|
|
289 |
(if epe-status--last-command-time
|
|
290 |
(let ((duration (time-to-seconds
|
|
291 |
(time-subtract (current-time) epe-status--last-command-time))))
|
|
292 |
(setq epe-status--last-command-time nil)
|
|
293 |
(if (> duration (or min-duration
|
|
294 |
epe-status-min-duration))
|
|
295 |
(funcall (or formatter
|
|
296 |
#'epe-status-formatter)
|
|
297 |
(current-time)
|
|
298 |
duration)
|
|
299 |
""))
|
|
300 |
(progn
|
|
301 |
(add-hook 'eshell-pre-command-hook #'epe-status--record)
|
|
302 |
"")))
|
229 | 303 |
|
230 | 304 |
;; tramp info
|
231 | 305 |
(defun epe-remote-p ()
|
|
253 | 327 |
|
254 | 328 |
(defun epe-git-p ()
|
255 | 329 |
"If you installed git and in a git project."
|
256 | |
(and (eshell-search-path "git")
|
257 | |
(vc-find-root (eshell/pwd) ".git")))
|
|
330 |
(unless (epe-remote-p) ; Work-around for issue #20
|
|
331 |
(and (eshell-search-path "git")
|
|
332 |
(vc-find-root (eshell/pwd) ".git"))))
|
258 | 333 |
|
259 | 334 |
(defun epe-git-short-sha1 ()
|
260 | 335 |
(epe-trim-newline (shell-command-to-string "git rev-parse --short HEAD")))
|
|
274 | 349 |
((string-match "^(HEAD detached at \\([[:word:]]+\\)" branch)
|
275 | 350 |
(concat epe-git-detached-HEAD-char (match-string 1 branch)))
|
276 | 351 |
(t branch))))
|
|
352 |
|
|
353 |
(defun epe-git-tag (&optional rev with-distance)
|
|
354 |
;; Inspired by `magit-get-current-tag'.
|
|
355 |
"Return the closest tag reachable from REV.
|
|
356 |
|
|
357 |
If optional REV is nil, then default to `HEAD'.
|
|
358 |
If optional WITH-DISTANCE is non-nil then return (TAG COMMITS),
|
|
359 |
if it is `dirty' return (TAG COMMIT DIRTY). COMMITS is the number
|
|
360 |
of commits in `HEAD' but not in TAG and DIRTY is t if there are
|
|
361 |
uncommitted changes, nil otherwise."
|
|
362 |
(let ((it (with-output-to-string
|
|
363 |
(with-current-buffer standard-output
|
|
364 |
(apply #'call-process "git" nil t nil "describe" "--long" "--tags"
|
|
365 |
(delq nil (list (and (eq with-distance 'dirty) "--dirty") rev)))))))
|
|
366 |
(unless (string-empty-p it)
|
|
367 |
(save-match-data
|
|
368 |
(string-match
|
|
369 |
"\\(.+\\)-\\(?:0[0-9]*\\|\\([0-9]+\\)\\)-g[0-9a-z]+\\(-dirty\\)?$" it)
|
|
370 |
(if with-distance
|
|
371 |
`(,(match-string 1 it)
|
|
372 |
,(string-to-number (or (match-string 2 it) "0"))
|
|
373 |
,@(and (match-string 3 it) (list t)))
|
|
374 |
(match-string 1 it))))))
|
277 | 375 |
|
278 | 376 |
(defun epe-git-dirty ()
|
279 | 377 |
"Return if your git is 'dirty'."
|
|
339 | 437 |
(epe-colorize-with-face
|
340 | 438 |
(concat (epe-remote-user) "@" (epe-remote-host) " ")
|
341 | 439 |
'epe-remote-face))
|
342 | |
(when epe-show-python-info
|
343 | |
(when (fboundp 'epe-venv-p)
|
344 | |
(when (and (epe-venv-p) venv-current-name)
|
345 | |
(epe-colorize-with-face (concat "(" venv-current-name ") ") 'epe-venv-face))))
|
|
440 |
(when (and epe-show-python-info (bound-and-true-p venv-current-name))
|
|
441 |
(epe-colorize-with-face (concat "(" venv-current-name ") ") 'epe-venv-face))
|
346 | 442 |
(let ((f (cond ((eq epe-path-style 'fish) 'epe-fish-path)
|
347 | 443 |
((eq epe-path-style 'single) 'epe-abbrev-dir-name)
|
348 | 444 |
((eq epe-path-style 'full) 'abbreviate-file-name))))
|
|
393 | 489 |
(epe-colorize-with-face
|
394 | 490 |
(concat (epe-remote-user) "@" (epe-remote-host) " ")
|
395 | 491 |
'epe-remote-face))
|
396 | |
(when epe-show-python-info
|
397 | |
(when (fboundp 'epe-venv-p)
|
398 | |
(when (and (epe-venv-p) venv-current-name)
|
399 | |
(epe-colorize-with-face (concat "(" venv-current-name ") ") 'epe-venv-face))))
|
|
492 |
(when (and epe-show-python-info (bound-and-true-p venv-current-name))
|
|
493 |
(epe-colorize-with-face (concat "(" venv-current-name ") ") 'epe-venv-face))
|
400 | 494 |
(epe-colorize-with-face (funcall
|
401 | 495 |
shrink-paths
|
402 | 496 |
(split-string
|
|
422 | 516 |
(concat
|
423 | 517 |
(if (epe-remote-p)
|
424 | 518 |
(progn
|
425 | |
(concat
|
426 | |
(epe-colorize-with-face "┌─[" 'epe-pipeline-delimiter-face)
|
427 | |
(epe-colorize-with-face (epe-remote-user) 'epe-pipeline-user-face)
|
428 | |
(epe-colorize-with-face "@" 'epe-pipeline-delimiter-face)
|
429 | |
(epe-colorize-with-face (epe-remote-host) 'epe-pipeline-host-face))
|
430 | |
)
|
|
519 |
(concat
|
|
520 |
(epe-colorize-with-face "┌─[" 'epe-pipeline-delimiter-face)
|
|
521 |
(epe-colorize-with-face (epe-remote-user) 'epe-pipeline-user-face)
|
|
522 |
(epe-colorize-with-face "@" 'epe-pipeline-delimiter-face)
|
|
523 |
(epe-colorize-with-face (epe-remote-host) 'epe-pipeline-host-face)))
|
431 | 524 |
(progn
|
432 | 525 |
(concat
|
433 | |
(epe-colorize-with-face "┌─[" 'epe-pipeline-delimiter-face)
|
434 | |
(epe-colorize-with-face (user-login-name) 'epe-pipeline-user-face)
|
435 | |
(epe-colorize-with-face "@" 'epe-pipeline-delimiter-face)
|
436 | |
(epe-colorize-with-face (system-name) 'epe-pipeline-host-face)))
|
437 | |
)
|
|
526 |
(epe-colorize-with-face "┌─[" 'epe-pipeline-delimiter-face)
|
|
527 |
(epe-colorize-with-face (user-login-name) 'epe-pipeline-user-face)
|
|
528 |
(epe-colorize-with-face "@" 'epe-pipeline-delimiter-face)
|
|
529 |
(epe-colorize-with-face (system-name) 'epe-pipeline-host-face))))
|
438 | 530 |
(concat
|
439 | 531 |
(epe-colorize-with-face "]──[" 'epe-pipeline-delimiter-face)
|
440 | 532 |
(epe-colorize-with-face (format-time-string "%H:%M" (current-time)) 'epe-pipeline-time-face)
|
441 | 533 |
(epe-colorize-with-face "]──[" 'epe-pipeline-delimiter-face)
|
442 | 534 |
(epe-colorize-with-face (concat (eshell/pwd)) 'epe-dir-face)
|
443 | 535 |
(epe-colorize-with-face "]\n" 'epe-pipeline-delimiter-face)
|
444 | |
(epe-colorize-with-face "└─>" 'epe-pipeline-delimiter-face)
|
445 | |
)
|
446 | |
(when epe-show-python-info
|
447 | |
(when (fboundp 'epe-venv-p)
|
448 | |
(when (and (epe-venv-p) venv-current-name)
|
449 | |
(epe-colorize-with-face (concat "(" venv-current-name ") ") 'epe-venv-face))))
|
|
536 |
(epe-colorize-with-face "└─>" 'epe-pipeline-delimiter-face))
|
|
537 |
(when (and epe-show-python-info (bound-and-true-p venv-current-name))
|
|
538 |
(epe-colorize-with-face (concat "(" venv-current-name ") ") 'epe-venv-face))
|
450 | 539 |
(when (epe-git-p)
|
451 | 540 |
(concat
|
452 | 541 |
(epe-colorize-with-face ":" 'epe-dir-face)
|
453 | 542 |
(epe-colorize-with-face
|
454 | 543 |
(concat (epe-git-branch)
|
455 | |
(epe-git-dirty)
|
456 | |
(epe-git-untracked)
|
457 | |
(let ((unpushed (epe-git-unpushed-number)))
|
458 | |
(unless (= unpushed 0)
|
459 | |
(concat ":" (number-to-string unpushed)))))
|
|
544 |
(epe-git-dirty)
|
|
545 |
(epe-git-untracked)
|
|
546 |
(let ((unpushed (epe-git-unpushed-number)))
|
|
547 |
(unless (= unpushed 0)
|
|
548 |
(concat ":" (number-to-string unpushed)))))
|
460 | 549 |
'epe-git-face)))
|
461 | 550 |
(epe-colorize-with-face " λ" 'epe-symbol-face)
|
462 | 551 |
(epe-colorize-with-face (if (= (user-uid) 0) "#" "") 'epe-sudo-symbol-face)
|
463 | 552 |
" "))
|
|
553 |
|
|
554 |
(defun epe-theme-multiline-with-status ()
|
|
555 |
"A simple eshell-prompt theme with information on its own line
|
|
556 |
and status display on command termination."
|
|
557 |
;; If the prompt spans over multiple lines, the regexp should match
|
|
558 |
;; last line only.
|
|
559 |
(setq eshell-prompt-regexp "^> ")
|
|
560 |
(concat
|
|
561 |
(epe-colorize-with-face (epe-status) 'epe-status-face)
|
|
562 |
(when (epe-remote-p)
|
|
563 |
(epe-colorize-with-face
|
|
564 |
(concat "(" (epe-remote-user) "@" (epe-remote-host) ")")
|
|
565 |
'epe-remote-face))
|
|
566 |
(when (and epe-show-python-info (bound-and-true-p venv-current-name))
|
|
567 |
(epe-colorize-with-face (concat "(" venv-current-name ") ") 'epe-venv-face))
|
|
568 |
(let ((f (cond ((eq epe-path-style 'fish) 'epe-fish-path)
|
|
569 |
((eq epe-path-style 'single) 'epe-abbrev-dir-name)
|
|
570 |
((eq epe-path-style 'full) 'abbreviate-file-name))))
|
|
571 |
(pcase (epe-extract-git-component (funcall f (eshell/pwd)))
|
|
572 |
(`(,prefix nil)
|
|
573 |
(format
|
|
574 |
(propertize "[%s]" 'face '(:weight bold))
|
|
575 |
(propertize prefix 'face 'epe-dir-face)))
|
|
576 |
(`(,prefix ,git-component)
|
|
577 |
(format
|
|
578 |
(epe-colorize-with-face "[%s%s@%s]" '(:weight bold))
|
|
579 |
(epe-colorize-with-face prefix 'epe-dir-face)
|
|
580 |
(if (string-empty-p git-component)
|
|
581 |
""
|
|
582 |
(concat "/"
|
|
583 |
(epe-colorize-with-face git-component 'epe-git-dir-face)))
|
|
584 |
(epe-colorize-with-face
|
|
585 |
(concat (or (epe-git-branch)
|
|
586 |
(epe-git-tag))
|
|
587 |
(epe-git-dirty)
|
|
588 |
(epe-git-untracked)
|
|
589 |
(let ((unpushed (epe-git-unpushed-number)))
|
|
590 |
(unless (= unpushed 0)
|
|
591 |
(concat ":" (number-to-string unpushed)))))
|
|
592 |
'epe-git-face)))))
|
|
593 |
(epe-colorize-with-face "\n>" '(:weight bold))
|
|
594 |
" "))
|
|
595 |
|
464 | 596 |
(provide 'eshell-prompt-extras)
|
465 | 597 |
|
466 | 598 |
;;; eshell-prompt-extras.el ends here
|