From f81f0c6a15ed31a44075fb0e7ea21623edda3dff Mon Sep 17 00:00:00 2001 From: Alexander Rosenberg Date: Sat, 21 Dec 2024 21:38:15 -0800 Subject: [PATCH] Rewrite eshell-starship.el --- elisp/eshell-starship.el | 1011 ++++++++++++++++++++++++++++++-------- init.el | 8 +- 2 files changed, 799 insertions(+), 220 deletions(-) diff --git a/elisp/eshell-starship.el b/elisp/eshell-starship.el index 12ccd73..08e2b6e 100644 --- a/elisp/eshell-starship.el +++ b/elisp/eshell-starship.el @@ -1,49 +1,193 @@ ;;; eshell-starship.el --- Starship-like (https://starship.rs) prompt for eshell -*- lexical-binding: t; -*- ;;; Commentary: ;;; Code: - (require 'vc) (require 'vc-git) (require 'eshell) (require 'cl-lib) -(defvar eshell-starship-modules nil - "List of modules used by eshell-starship. -This should be an alist of (name function). The macro -`eshell-starship-defmodule' can help modify this list.") +;;; Configuration options +(defgroup eshell-starship nil + "Starship-like (starship.rs) prompt for `eshell'." + :version "0.0.1" + :prefix 'eshell-starship- + :group 'eshell) + +(defvar-local eshell-starship--current-explain-buffer nil + "The eshell-starship explain buffer for this eshell buffer.") + +(defcustom eshell-starship-explain-auto-update t + "Non-nil if eshell-starship explain buffers shoul auto-update." + :group 'eshell-starship + :tag "Auto-update explain buffers" + :type 'boolean) + +(defun eshell-starship--defcustom-setter (sym val) + "Set SYM to VAL (using `set-default-toplevel-value'). +This will also update all eshell-starship explain buffers that need updating." + (set-default-toplevel-value sym val) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (and (derived-mode-p 'eshell-mode) + (buffer-live-p eshell-starship--current-explain-buffer)) + (with-current-buffer eshell-starship--current-explain-buffer + (when eshell-starship-explain-auto-update + (revert-buffer))))))) + +(defcustom eshell-starship-module-order + '("remote" "cwd" "vc" t "cmd-time" "arrow") + "The order of modules for eshell-starship. +This is a list with each element being a module name. The special value t can +appear at most once to denote \"all remaining modules\"." + :group 'eshell-starship + :tag "Module order" + :type '(repeat (choice (const :tag "Remaining modules" t) + (string :tag "Module"))) + :set 'eshell-starship--defcustom-setter) + +(defcustom eshell-starship-disabled-modules '() + "List of disabled eshell-starship modules." + :group 'eshell-starship + :tag "Disabled modules" + :type '(repeat (string :tag "Module")) + :set 'eshell-starship--defcustom-setter) + +(defcustom eshell-starship-explain-suppress-refresh-messages nil + "Weather to suppress messages during eshell-starship explore refreshes." + :group 'eshell-starship + :tag "Suppress eshell-starship explore refresh messages" + :type 'boolean) + + +;;; Module API +(defvar eshell-starship-modules (make-hash-table :test 'equal) + "List of modules used by eshell-starship.") (defvar-local eshell-starship--module-cache nil - "Hash table to hold module cache for eshell-starship.") + "Hash table mapping modules to a list of their last output. +The entries of this hash table are of the the form (VALID OUTPUT LAST-TIMES). +LAST-TIMES is a list of the 10 last execution times for this module.") -(defvar-local eshell-starship--files-name-cache nil - "Cache of file names last time eshell-starship checked.") +(defclass eshell-starship-module () + ((name :initarg :name + :accessor eshell-starship-module-name + :type string + :documentation "The name of this module.") + (precmd-action :initarg :precmd-action + :initform nil + :accessor eshell-starship-module-precmd-action + :type (or function null) + :documentation + "A function to run before each command is run.") + (postcmd-action :initarg :postcmd-action + :initform nil + :accessor eshell-starship-module-postcmd-action + :type (or function null) + :documentation + "A function to run after each command is run.") + (predicate :initarg :predicate + :initform 'ignore + :accessor eshell-starship-module-predicate + :type function + :documentation + "A function that should return non-nil if the module should be run.") + (files :initarg :files + :initform nil + :accessor eshell-starship-module-files + :type list + :documentation + "A list of files that indicate that the module should be run.") + (dirs :initarg :dirs + :initform nil + :accessor eshell-starship--module-dirs + :type list + :documentation + "A list of directories that indicate that the module should be run.") + (extensions :initarg :extensions + :initform nil + :accessor eshell-starship-module-extensions + :type list + :documentation + "A list of extensions that indicate that the module should be run.") + (prefix :initarg :prefix + :initform "" + :accessor eshell-starship-module-prefix + :type string + :documentation "Text to be placed before the module's icon. This is +not colored.") + (icon :initarg :icon + :initform "" + :accessor eshell-starship-module-icon + :type string + :documentation "The modules icon. This is colored.") + (postfix :initarg :postfix + :initform "" + :accessor eshell-starship-module-prefix + :type string + :documentation "Text to be placed after the module's content. This is +not colored.") + (color :initarg :color + :initform nil + :accessor eshell-starship-module-color + :type (or null string) + :documentation "The color to give the module's icon and main text. +Use `list-colors-display' to get a list of some possible values. This can also +be nil.") + (allow-remote :initarg :allow-remote + :initform t + :accessor eshell-starship-module-allow-remote-p + :type boolean + :documentation "Weather the module should be run if +`default-directory' is a `file-remote-p'.") + (action :initarg :action + :initform 'ignore + :accessor eshell-starship-module-action + :type function + :documentation "A function that produces the main text for the +module.") + (reload-on :initarg :reload-on + :initform 'never + :accessor eshell-starship-module-reload-on + :type (or (member never always cwd) + (list-of (member never always cwd))) + :documentation "A list of times when this module should be +reloaded. Current possible values are: + - never (or an empty list): don't ever re-run this module + - always: re-run this module every time the prompt is updated + - cwd: re-run this module when the CWD changes") + (doc :initarg :doc + :initform "No documentation provided." + :accessor eshell-starship-module-doc + :type string + :documentation "The documentation for this module.")) + (:documentation "Class for eshell-starship modules.")) -(cl-defmacro eshell-starship-defmodule (name &key pred files dirs exts icon color - allow-remote action reload-on) - "Define an eshell-starship module called NAME. -The module will be added to `eshell-starship-modules'. - :PRED - a function that should return t if the module should be run - :FILES - a list of file names and wildcard expansions that will be used to - determine if the module should be run - :EXTS - save as FILES but mach any file with the given extensions - :DIRS - same as FILES, but for directories - :ICON - this is the string to print before the modules text - :COLOR - the color to print the modules text in - :ALLOW-REMOTE - weather to allow the module to run on remote machines - :ACTION - a function that will return the module text, or nil - :RELOAD-ON - when to re-run the module. List of \\='cwd, \\='files, - \\='always, or \\='never (same as nil)" +(defun eshell-starship--defmodule-real (name &rest opts) + "Do the work of `eshell-starship-defmodule'. +NAME is the module name (a symbol) and OPTS is the list of options to pass to +the module's constructor." + (let ((module (apply 'make-instance 'eshell-starship-module + :name (symbol-name name) opts))) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (hash-table-p eshell-starship--module-cache) + (remhash (symbol-name name) eshell-starship--module-cache)))) + ;; This returns module + (puthash (symbol-name name) module eshell-starship-modules))) + +(defmacro eshell-starship-defmodule (name &rest opts) + "Create a new eshell-starship named NAME module with OPTS." (declare (indent defun)) - `(setf (alist-get ',name eshell-starship-modules) - (list ,pred ,files ,dirs ,exts ,icon ,color ,allow-remote ,action - (ensure-list ,reload-on)))) + `(eshell-starship--defmodule-real ',name ,@opts)) + +;;; Utility functions (cl-defmacro eshell-starship-find-version-function (command pattern - &rest format) - "Return a lambda that calls COMMAND. + &rest format) + "Return a version finding function for COMMAND. COMMAND is in the form of (exec args...). The temp buffer that was used to run -COMMAND will then have `re-search-forward' run with PATTERN and FORMAT should -arguments to pass to `concat' to format the output." +COMMAND will then have `re-search-forward' run with PATTERN. FORMAT will then +be passed verbatim as the arguments to `concat'." (declare (indent defun)) `(lambda () (with-temp-buffer @@ -52,136 +196,21 @@ arguments to pass to `concat' to format the output." (when (re-search-forward ,pattern nil t) (concat ,@format)))))) -(eshell-starship-defmodule cc - :exts '("c" "h") - :icon "C" - :color "green yellow" - :allow-remote nil - :reload-on 'cwd - :action (eshell-starship-find-version-function - ("cc" "-v") - "^\\([-a-zA-Z]+\\) version \\([0-9]+\\.[0-9]+\\.[0-9]+\\)" - "v" (match-string 2) "-" (match-string 1))) - -(eshell-starship-defmodule rust - :exts '("rs") - :files '("Cargo.toml") - :icon "🦀" - :color "red" - :allow-remote nil - :reload-on 'cwd - :action (eshell-starship-find-version-function - ("rustc" "--version") - "^rustc \\([0-9]+\\.[0-9]+\\.[0-9]+\\)" - "v" (match-string 1))) - -(eshell-starship-defmodule cmake - :files '("CMakeLists.txt" "CMakeCache.txt") - :icon "󰔶" - :color "blue" - :allow-remote nil - :reload-on 'cwd - :action (eshell-starship-find-version-function - ("cmake" "--version") - "cmake version \\([0-9]+\\.[0-9]+\\.[0-9]+\\)" - "v" (match-string 1))) - -(require 'inf-lisp nil t) -(when (featurep 'inf-lisp) - (eshell-starship-defmodule common-lisp - :exts '("asd" "lisp") - :icon "" - :color "green" - :allow-remote nil - :reload-on 'cwd - :action (eshell-starship-find-version-function - (inferior-lisp-program "--version") - "[a-zA-Z]+ [0-9.]+" - (match-string 0)))) - -(eshell-starship-defmodule elisp - :exts '("el" "elc" "eln") - :icon "" - :color "dark orchid" - :allow-remote nil - :reload-on 'never - :action (lambda () - emacs-version)) - -(eshell-starship-defmodule java - :exts '("java" "class" "gradle" "jar" "clj" "cljc") - :files '("pom.xml" "build.gradle.kts" "build.sbt" ".java-version" "deps.edn" - "project.clj" "build.boot" ".sdkmanrc") - :icon "☕" - :color "dark red" - :allow-remote nil - :reload-on 'cwd - :action (eshell-starship-find-version-function - ("java" "-version") - "version \"\\([0-9]+\\)\"" - "v" (match-string 1))) - -(defun eshell-starship--clear-caches (&rest flags) - "Clear each cache entry with a \\=:reload-on of FLAGS." - (cl-loop for module in eshell-starship-modules - for reload-on = (cl-tenth module) - when (cl-intersection reload-on flags) - do (remhash (cl-first module) eshell-starship--module-cache))) - -(defun eshell-starship--cwd-clear-caches () - "Clear caches that should be cleared on cwd for eshell-starship." - (eshell-starship--clear-caches 'cwd 'files)) - -(defun eshell-starship--maybe-files-clear-caches () - "Clear caches that should be claered if files changed. -This will only clear the caches if the files actually changed." - (let ((files (cons 'set (directory-files default-directory)))) - (unless (equal files eshell-starship--files-name-cache) - (setq eshell-starship--files-name-cache files) - (eshell-starship--clear-caches 'files)))) - -(defun eshell-starship--exts-exist-p (&rest exts) - "Test if any files with EXTS at the end of their name exist in default dir." - (catch 'found - (dolist (ext exts) - (when (seq-filter #'(lambda (name) - (not (string-prefix-p "." name))) - (file-expand-wildcards (concat "*." ext))) - (throw 'found t))))) - -(defun eshell-starship--files-exist-p (&rest names) - "Test if any of NAMES exists and are files in default directory." - (catch 'found - (dolist (name names) - (when (file-exists-p name) - (throw 'found t))))) - -(defun eshell-starship--dirs-exist-p (&rest names) - "Test if any of NAMES exists and are files in default directory." - (catch 'found - (dolist (name names) - (when (file-directory-p name) - (throw 'found t))))) - -(defun eshell-starship--execute-modules () - "Execute all the modules in `eshell-starship-modules'." - (cl-loop - for (name pred files dirs exts icon color allow-remote action reload-on) - in eshell-starship-modules - when (and (or allow-remote (not (file-remote-p default-directory))) - (or (and files (apply 'eshell-starship--files-exist-p files)) - (and dirs (apply' eshell-starship--dirs-exist-p dirs)) - (and exts (apply' eshell-starship--exts-exist-p exts)) - (and pred (funcall pred)))) - concat (if-let (cache-val (gethash name eshell-starship--module-cache)) - cache-val - (if-let ((result (funcall action)) - (mod-string (concat " via " (propertize (concat icon " " result) - 'face `(:foreground ,color))))) - (unless (member 'always reload-on) - (puthash name mod-string eshell-starship--module-cache)) - result)))) +(cl-defun eshell-starship-format-span (span &optional (places 0)) + "Format SPAN (in seconds) as \"XhXmXs\". +The number is rounded to PLACES before being rendered." + (let* ((ispan (round span)) + (hours (/ ispan 3600)) + (mins (% (/ ispan 60) 60)) + (secs (mod span 60))) + (concat (when (/= hours 0) + (format "%dh" hours)) + (when (or (/= mins 0) (/= hours 0)) + (format "%dm" mins)) + (format (format "%%.%dfs" places) secs)))) + +;;; CWD Module (defun eshell-starship--replace-home-with-tilda (path) "If PATH beings with $HOME (the environment variable), replace it with ~." (let ((home (getenv "HOME"))) @@ -206,13 +235,24 @@ Example: (defun eshell-starship--get-current-dir () "Get dir for `eshell-starship--prompt-function'." - (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)))) + (concat + (propertize (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))) + 'face '(:foreground "dark turquoise")) + (unless (file-writable-p default-directory) + " "))) +(eshell-starship-defmodule cwd + :predicate 'always + :reload-on 'cwd + :action 'eshell-starship--get-current-dir + :doc "The current working directory.") + + +;;; VC (git) module (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) @@ -339,7 +379,7 @@ For example, a revert. If there is no current operation, return nil." (if (string= "(detached)" head) (propertize (concat " (" (substring oid 0 7) ")") 'face '(:foreground "lawn green")) - (propertize (concat " 󰊢 " head) + (propertize (concat "󰊢 " head) 'face '(:foreground "medium purple"))) (unless (string-empty-p file-status) (propertize (concat " [" file-status "]") @@ -353,70 +393,396 @@ For example, a revert. If there is no current operation, return nil." "Get vc status for `eshell-starship--prompt-function'." (if-let (backend (vc-responsible-backend default-directory t)) (if (eq backend 'Git) - (eshell-starship--git-status) + (let ((status (eshell-starship--git-status))) + (and (not (zerop (length status))) status)) (propertize - (concat "  " (downcase (symbol-name backend))) + (concat " " (downcase (symbol-name backend))) 'face '(:foreground "purple"))))) +(eshell-starship-defmodule vc + :predicate 'always + :allow-remote nil + :reload-on 'always + :action 'eshell-starship--vc-status + :doc "The working directory's version control status.") + + +;;; Timer module (defvar-local eshell-starship--last-start-time nil "Start time of last eshell command.") -(defun eshell-starship--timer-pre-cmd () - "Command run before each eshell program to record the time." - (setq eshell-starship--last-start-time (current-time))) +(defvar-local eshell-starship--last-end-time nil + "End time of last eshell command.") -(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--last-command-time () + "Return the prompt component for the time of the last command." + (prog1 + (and eshell-starship--last-start-time + eshell-starship--last-end-time + (eshell-starship-format-span (- eshell-starship--last-end-time + eshell-starship--last-start-time))) + (setq eshell-starship--last-start-time nil))) -(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--last-start-time) - (len (time-subtract end-time - eshell-starship--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"))))) +(eshell-starship-defmodule cmd-time + :prefix "took " + :color "gold1" + :reload-on 'always + :precmd-action (lambda () + (setq eshell-starship--last-start-time (float-time))) + :postcmd-action (lambda () + (setq eshell-starship--last-end-time (float-time))) + :predicate (lambda () + (and eshell-starship--last-start-time + eshell-starship--last-end-time + (<= 3 (- eshell-starship--last-end-time + eshell-starship--last-start-time)))) + :action 'eshell-starship--last-command-time + :doc "The amount of time it took the last command to execute.") + + +;;; Language modules +(eshell-starship-defmodule cc + :extensions '("c" "h") + :prefix "via " + :icon "C " + :color "spring green" + :allow-remote nil + :reload-on 'cwd + :action (eshell-starship-find-version-function + ("cc" "-v") + "^\\([-a-zA-Z]+\\) version \\([0-9]+\\.[0-9]+\\.[0-9]+\\)" + "v" (match-string 2) "-" (match-string 1)) + :doc "Your C compiler version.") + +(eshell-starship-defmodule c++ + :extensions '("cpp" "cc" "cxx" "hpp" "hh" "hxx") + :prefix "via " + :icon " " + :color "royal blue" + :allow-remote nil + :reload-on 'cwd + :action (eshell-starship-find-version-function + ("cpp" "--version") + "\\(GCC\\|clang\\).+?\\([0-9]+\\.[0-9]+\\.[0-9]+\\)" + "v" (match-string 2) "-" (downcase (match-string 1))) + :doc "Your C++ compiler version.") + +(eshell-starship-defmodule rust + :extensions '("rs") + :files '("Cargo.toml") + :prefix "via " + :icon "🦀 " + :color "red" + :allow-remote nil + :reload-on 'cwd + :action (eshell-starship-find-version-function + ("rustc" "--version") + "^rustc \\([0-9]+\\.[0-9]+\\.[0-9]+\\)" + "v" (match-string 1)) + :doc "Your Rust compiler version.") + +(eshell-starship-defmodule cmake + :files '("CMakeLists.txt" "CMakeCache.txt") + :prefix "via " + :icon "󰔶 " + :color "blue" + :allow-remote nil + :reload-on 'cwd + :action (eshell-starship-find-version-function + ("cmake" "--version") + "cmake version \\([0-9]+\\.[0-9]+\\.[0-9]+\\)" + "v" (match-string 1)) + :doc "Your CMake version.") + +(require 'inf-lisp nil t) +(when (featurep 'inf-lisp) + (eshell-starship-defmodule common-lisp + :extensions '("asd" "lisp") + :prefix "via " + :icon " " + :color "green yellow" + :allow-remote nil + :reload-on 'cwd + :action (eshell-starship-find-version-function + (inferior-lisp-program "--version") + "[a-zA-Z]+ [0-9.]+" + (match-string 0)) + :doc "Your current inferior-lisp program.")) + +(eshell-starship-defmodule elisp + :extensions '("el" "elc" "eln") + :prefix "via " + :icon " " + :color "dark orchid" + :allow-remote nil + :reload-on 'never + :action (lambda () + emacs-version) + :doc "The current emacs-version.") + +(eshell-starship-defmodule java + :extensions '("java" "class" "gradle" "jar" "clj" "cljc") + :files '("pom.xml" "build.gradle.kts" "build.sbt" ".java-version" "deps.edn" + "project.clj" "build.boot" ".sdkmanrc") + :prefix "via " + :icon "☕ " + :color "dark red" + :allow-remote nil + :reload-on 'cwd + :action (eshell-starship-find-version-function + ("java" "-version") + "version \"\\([0-9]+\\)\"" + "v" (match-string 1)) + :doc "Your Java version.") + +(eshell-starship-defmodule zig + :extensions '("zig") + :prefix "via " + :icon "↯ " + :color "yellow" + :allow-remote nil + :reload-on 'cwd + :action (eshell-starship-find-version-function + ("zig" "version") + ".+" + "v" (match-string 0)) + :doc "Your Zig version.") + +(eshell-starship-defmodule python + :extensions '("py" "ipynb") + :files '(".python-version" "Pipfile" "__init__.py" "pyproject.toml" + "requirements.txt" "setup.py" "tox.ini" "pixi.toml") + :prefix "via " + :icon "🐍 " + :color "#CECB00" + :allow-remote nil + :reload-on 'cwd + :action (eshell-starship-find-version-function + ("python" "--version") + "^Python \\([0-9.]+\\)" + "v" (match-string 1)) + :doc "Your current system-wide Python version.") + +(eshell-starship-defmodule php + :extensions '("php")' + :files '("composer.json" ".php-version") + :prefix "via " + :icon "🐘 " + :color "#AFAFFF" + :allow-remote nil + :reload-on 'cwd + :action (eshell-starship-find-version-function + ("php" "--version") + "^PHP \\([0-9.]+\\)" + "v" (match-string 1)) + :doc "Your current PHP version.") + +(eshell-starship-defmodule node + :extensions '("js" "mjs" "cjs" "ts" "mts" "cts") + :files '("package.json" ".node-version" ".nvmrc") + :dirs '("node_modules") + :icon " " + :color "green" + :allow-remote nil + :reload-on 'cwd + :action (eshell-starship-find-version-function + ("node" "--version") + ".+" (match-string 0)) + :doc "Your current NodeJS version.") + + +;;; Misc modules +(eshell-starship-defmodule remote + :icon "🌐" + :color "light blue" + :predicate (lambda () + (file-remote-p default-directory)) + :doc "A small icon if the working directory is remote.") + +(eshell-starship-defmodule arrow + :predicate 'always + :reload-on 'always + :action (lambda () + (concat + (propertize "\n" 'read-only t 'rear-nonsticky t) + (propertize + "❯ " 'face `(:foreground + ,(if (= eshell-last-command-status 0) + "lime green" + "red")) + 'rear-nonsticky t))) + :doc "An arrow that appears next to where you type.") + + +;;; Driver code + +(defun eshell-starship-clear-cache (&rest flags) + "Clear each cache entry with a \\=:reload-on of FLAGS. +If any of flags is t, clear all caches." + (interactive '(t) eshell-starship) + (cl-loop with force-clear = (member t flags) + for module being the hash-values of eshell-starship-modules + do (with-slots (name reload-on) module + (when-let (((or force-clear + (cl-intersection (ensure-list reload-on) flags))) + (cur-entry (gethash name + eshell-starship--module-cache))) + (setf (car cur-entry) nil))))) + +(defun eshell-starship--cwd-clear-caches () + "Clear caches that should be cleared on cwd for eshell-starship." + (eshell-starship-clear-cache 'cwd)) + +(defun eshell-starship--exts-exist-p (&rest exts) + "Test if any files with EXTS at the end of their name exist. +The test is performed relative to `default-directory'." + (catch 'found + (dolist (ext exts) + (when (seq-filter #'(lambda (name) + (not (string-prefix-p "." name))) + (file-expand-wildcards (concat "*." ext))) + (throw 'found t))))) + +(defun eshell-starship--files-exist-p (&rest names) + "Test if any of NAMES exist and are normal files. +The test is performed relative to `default-directory'." + (catch 'found + (dolist (name names) + (when (file-exists-p name) + (throw 'found t))))) + +(defun eshell-starship--dirs-exist-p (&rest names) + "Test if any of NAMES exist and are directories. +The test is performed relative to `default-directory'." + (catch 'found + (dolist (name names) + (when (file-directory-p name) + (throw 'found t))))) + +(cl-defun eshell-starship--display-module-p (module &optional + (dir default-directory)) + "Return non-nil if MODULE should be displayed while in DIR." + (with-slots (name predicate files dirs extensions allow-remote) module + (and (not (cl-member name eshell-starship-disabled-modules :test 'equal)) + (or allow-remote (not (file-remote-p dir))) + (let ((default-directory dir)) + (or (and files (apply 'eshell-starship--files-exist-p files)) + (and dirs (apply' eshell-starship--dirs-exist-p dirs)) + (and extensions (apply' eshell-starship--exts-exist-p extensions)) + (and predicate (funcall predicate))))))) + +(defun eshell-starship--execute-module (module) + "Run the module MODULE and return its output. +Also cache the time it took to run it and its output." + (with-slots (name action prefix postfix icon color) module + (let ((oldtimes (cl-third (gethash name eshell-starship--module-cache))) + start-time result end-time) + (setq start-time (float-time) + result (funcall action) + end-time (float-time)) + (when-let ((result) + (output (concat prefix + (if color + (propertize (concat icon result) + 'face `(:foreground ,color)) + (concat icon result)) + postfix))) + (puthash name (list t output (cons (- end-time start-time) + (take 9 oldtimes))) + eshell-starship--module-cache) + output)))) + +(defun eshell-starship--execute-modules () + "Execute all the modules in `eshell-starship-modules'. +Return a hash table mapping module names to their output." + (cl-loop + with output = (make-hash-table :test 'equal) + for module being the hash-values of eshell-starship-modules + for name = (eshell-starship-module-name module) + for reload-on = (ensure-list (eshell-starship-module-reload-on module)) + for cache-entry = (gethash name eshell-starship--module-cache) + do (when (eshell-starship--display-module-p module) + (if (and (not (member 'always reload-on)) (car cache-entry)) + (puthash name (cl-second cache-entry) output) + (puthash name (eshell-starship--execute-module module) output))) + finally (maphash (lambda (k v) + (unless v + (remhash k output))) + output) + finally return output)) + +(defun eshell-starship--run-module-precmd-actions () + "Run the pre-command action for each module." + (cl-loop for module being the hash-values of eshell-starship-modules + for precmd-action = (eshell-starship-module-precmd-action module) + when precmd-action + do (funcall precmd-action))) + +(defun eshell-starship--run-module-postcmd-actions () + "Run the post-command action for each module." + (cl-loop for module being the hash-values of eshell-starship-modules + for postcmd-action = (eshell-starship-module-postcmd-action module) + when postcmd-action + do (funcall postcmd-action))) + +(defun eshell-starship--build-module-string () + "Build a space-separated string of module outputs." + (let ((output (eshell-starship--execute-modules)) + pre post found-rest) + (dolist (cur-name (mapcar (lambda (name) + (if (and (not (eq name t)) (symbolp name)) + (symbol-name name) + name)) + eshell-starship-module-order)) + (cond + ((and (eq cur-name t) found-rest) + (warn "t appears more than once in `eshell-starship-module-order")) + ((eq cur-name t) + (setq found-rest t)) + ((not (gethash cur-name output)) + ;; skip + ) + (found-rest + (push (gethash cur-name output) post) + (remhash cur-name output)) + (t + (push (gethash cur-name output) pre) + (remhash cur-name output)))) + (mapconcat 'identity + (nconc (nreverse pre) (hash-table-values output) (nreverse post)) + " "))) + +(defun eshell-starship--render-prompt () + "Actually produce the prompt." + (concat + (unless (< (line-number-at-pos) 3) + "\n") + (eshell-starship--build-module-string))) + +(defvar-local eshell-starship--last-prompt-info nil + "A list of the last prompt and the time it took to render it.") (defun eshell-starship--prompt-function () "Function for `eshell-prompt-function'." - (eshell-starship--maybe-files-clear-caches) - (let* ((end-time (current-time)) - (dir (eshell-starship--get-current-dir)) - (prompt (concat - "\n" - (if (file-remote-p default-directory) - (propertize "🌐 " 'face '(:foreground "light blue"))) - (propertize dir 'face '(:foreground "dark turquoise")) - (unless (file-writable-p default-directory) - " ") - (eshell-starship--vc-status) - (eshell-starship--execute-modules) - (eshell-starship--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--last-start-time nil) + (let (start-time prompt end-time) + (setq start-time (float-time) + prompt (eshell-starship--render-prompt) + end-time (float-time) + eshell-starship--last-prompt-info + (list prompt (- end-time start-time))) + (when (buffer-live-p eshell-starship--current-explain-buffer) + (with-current-buffer eshell-starship--current-explain-buffer + (when eshell-starship-explain-auto-update + (let ((eshell-starship-explain-suppress-refresh-messages t)) + (revert-buffer))))) prompt)) (defvar-local eshell-starship--restore-state nil "State of various variables set by `eshell-starship-prompt-mode'.") +(defvar-local eshell-starship--explain-eshell-buffer nil + "The eshell buffer backing this eshell-starship-explain buffer.") + (defun eshell-starship--enable () "Enable eshell-starship." (setq-local eshell-starship--restore-state @@ -424,16 +790,26 @@ END-TIME is the time when the command finished executing." 'eshell-starship--prompt-function eshell-prompt-regexp "^❯ " eshell-highlight-prompt nil) - eshell-starship--module-cache (make-hash-table :test 'eq)) - (add-hook 'eshell-pre-command-hook #'eshell-starship--timer-pre-cmd nil t) + eshell-starship--module-cache (make-hash-table :test 'equal)) + (add-hook 'eshell-pre-command-hook + #'eshell-starship--run-module-precmd-actions nil t) + (add-hook 'eshell-post-command-hook + #'eshell-starship--run-module-postcmd-actions nil t) (add-hook 'eshell-directory-change-hook #'eshell-starship--cwd-clear-caches nil t)) (defun eshell-starship--disable () "Disable eshell-starship." - (setq-local eshell-starship--module-cache nil) + (when eshell-starship--current-explain-buffer + (with-current-buffer eshell-starship--current-explain-buffer + (setq eshell-starship--explain-eshell-buffer nil))) + (setq-local eshell-starship--module-cache nil + eshell-starship--current-explain-buffer nil) (buffer-local-restore-state eshell-starship--restore-state) - (remove-hook 'eshell-pre-command-hook #'eshell-starship--timer-pre-cmd t) + (remove-hook 'eshell-pre-command-hook + #'eshell-starship--run-module-precmd-actions t) + (remove-hook 'eshell-post-command-hook + #'eshell-starship--run-module-postcmd-actions t) (remove-hook 'eshell-directory-change-hook #'eshell-starship--cwd-clear-caches t)) @@ -447,5 +823,204 @@ END-TIME is the time when the command finished executing." (eshell-starship--enable) (eshell-starship--disable))) + +;;; Explain buffer +(defface eshell-starship--heading + '((t (:height 1.2 :weight bold) )) + "Face for showing headings in `eshell-starship-explain' buffers.") + +(defun eshell-starship--insert-prompt-string (str &optional prefix first-prefix) + "Insert STR, a prompt string, into the current buffer. +This just cleans up STR a bit before inserting it. Also, if PREFIX is non-nil, +it will be inserted at the start of each line. If FIRST-PREFIX is non-nil, it +will be used specially as the first line's prefix." + (cl-loop with first-line = t + with blank-count = 0 + with found-start = nil + for line in (string-lines str) + for empty = (zerop (length line)) + while line + do + (if empty + (when found-start + (cl-incf blank-count)) + (setq found-start t) + (dotimes (_ blank-count) + (insert "\n")) + (if (and first-line first-prefix) + (progn + (insert first-prefix) + (setq first-line nil)) + (insert prefix)) + (insert line) + (insert "\n")))) + +(defun eshell-starship--explain-insert-module (module &optional no-output) + "Insert information about MODULE at point. +If NO-OUTPUT is non-nil, don't insert the modules previous output." + (with-slots (name doc) module + (let ((bullet-char (if (char-displayable-p ?\•) + ?\• + ?\-)) + (cache-entry (gethash + name (buffer-local-value + 'eshell-starship--module-cache + eshell-starship--explain-eshell-buffer)))) + (insert (format " %c %s - %s\n" + bullet-char + (propertize name + 'face 'font-lock-keyword-face) + doc)) + (unless no-output + (unless (eshell-starship--display-module-p + module + (buffer-local-value 'default-directory + eshell-starship--explain-eshell-buffer)) + (insert " (This module is hidden.)\n")) + (if (not (cl-first cache-entry)) + (insert " This module has no cached output.\n") + (insert " Last output was:\n") + (eshell-starship--insert-prompt-string (cl-second cache-entry) + " " + " \"") + (forward-line -1) + (end-of-line) + (insert "\"") + (forward-line) + (insert (format " It took %s.\n" + (eshell-starship-format-span + (car (cl-third cache-entry)) 3)))) + (insert "\n"))))) + +(defun eshell-starship--explain-insert-enabled () + "Insert an explanation of enabled modules at point." + (let ((rest-modules (copy-hash-table eshell-starship-modules)) + rest-point) + (dolist (cur-name eshell-starship-module-order) + (unless (cl-member cur-name eshell-starship-disabled-modules + :test 'equal) + (if (eq cur-name t) + (setq rest-point (point)) + (when-let ((module (gethash cur-name eshell-starship-modules))) + (eshell-starship--explain-insert-module module) + (remhash cur-name rest-modules))))) + (save-excursion + (goto-char rest-point) + (cl-loop for module being the hash-values of rest-modules + using (hash-keys name) + unless (cl-member name eshell-starship-disabled-modules + :test 'equal) + do (eshell-starship--explain-insert-module module))))) + +(defun eshell-starship--explain-format-buffer () + "Fill the current buffer with content for `eshell-starship-explain'." + (unless (buffer-live-p eshell-starship--explain-eshell-buffer) + (error "Parent Eshell buffer is gone (or no longer using eshell-starship)")) + (erase-buffer) + (cl-flet ((heading (txt) + (propertize txt 'face 'eshell-starship--heading))) + (cl-destructuring-bind (&optional last-prompt last-time) + (buffer-local-value 'eshell-starship--last-prompt-info + eshell-starship--explain-eshell-buffer) + (when (and last-prompt last-time) + (insert "The last prompt was:\n") + (eshell-starship--insert-prompt-string last-prompt " ") + (insert + (format "\nIt was rendered in %s.\n\n" + (eshell-starship-format-span last-time 3))))) + (insert (heading "The following modules are enabled:\n")) + (eshell-starship--explain-insert-enabled) + (if (null eshell-starship-disabled-modules) + (insert (heading "There are no disabled modules.")) + (insert (heading "The following modules are disabled:\n")) + (dolist (name eshell-starship-disabled-modules) + (when-let ((module (gethash name eshell-starship-modules))) + (eshell-starship--explain-insert-module module t))) + ;; get rid of newline + (delete-char -1)))) + +(defun eshell-starship--explain-revert (_ignore-auto _noconfirm) + "Revert function for eshell-starship explain buffers. +_IGNORE-AUTO and _NOCONFIRM are ignored." + (let ((save (point)) + (inhibit-read-only t)) + (eshell-starship--explain-format-buffer) + (goto-char save)) + (unless eshell-starship-explain-suppress-refresh-messages + (message "Refreshed eshell-starship explain buffer"))) + +;;;###autoload +(defun eshell-starship-explain-toggle-auto-update-mode (&optional arg) + "Toggle `eshell-starship-explain-auto-update' in the current buffer. +If ARG is negative, disable it. If ARG is positive, enable it. Otherwise, +toggle it." + (interactive "P" eshell-starship-explain-mode) + (unless (derived-mode-p 'eshell-starship-explain-mode) + (error "Not an eshell-starship explain buffer")) + (if (not arg) + (cl-callf not eshell-starship-explain-auto-update) + (let ((num (prefix-numeric-value arg))) + (setq eshell-starship-explain-auto-update (<= 0 num)))) + (when eshell-starship-explain-auto-update + (revert-buffer)) + (force-mode-line-update) + (message "%s auto-updating." + (if eshell-starship-explain-auto-update + "Enabled" + "Disabled"))) + +;;;###autoload +(defvar-keymap eshell-starship-explain-mode-map + :doc "Keymap for `eshell-starship-explain-mode'." + :parent special-mode-map + :suppress t + "a" #'eshell-starship-explain-toggle-auto-update-mode + "r" #'revert-buffer) + +(define-derived-mode eshell-starship-explain-mode nil + "Eshell-Starship Explain" + "Major mode for `eshell-starship-explain' buffers." + :group 'eshell-starship + :interactive nil + (setq-local mode-name + '("Eshell-Starship Explain" + (eshell-starship-explain-auto-update "/a")) + display-line-numbers nil + revert-buffer-function + 'eshell-starship--explain-revert)) + +;;;###autoload +(defun eshell-starship-setup-evil-keybindings () + "Setup keybindings for `evil-mode' for eshell-starship." + (require 'evil) + (when (fboundp 'evil-define-key*) + (evil-define-key* '(motion normal) eshell-starship-explain-mode-map + "a" #'eshell-starship-explain-toggle-auto-update-mode + "r" #'revert-buffer))) + +;;;###autoload +(defun eshell-starship-explain () + "Show some information about the current prompt." + (interactive nil eshell-mode) + (unless (derived-mode-p 'eshell-mode) + (error "Current buffer is not in eshell-mode. Nothing to explain")) + (let ((eshell-buffer (current-buffer)) + (explain-buffer (get-buffer-create "*Eshell-Starship Explain*"))) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (cond + ((eq buffer eshell-buffer) + (setq eshell-starship--current-explain-buffer explain-buffer)) + ((derived-mode-p 'eshell-mode) + (setq eshell-starship--current-explain-buffer nil))))) + (with-current-buffer explain-buffer + (unless (derived-mode-p 'eshell-starship-explain-mode) + (eshell-starship-explain-mode)) + (setq eshell-starship--explain-eshell-buffer eshell-buffer) + (save-excursion + (let ((inhibit-read-only t)) + (eshell-starship--explain-format-buffer))) + (pop-to-buffer (current-buffer))))) + (provide 'eshell-starship) ;;; eshell-starship.el ends here diff --git a/init.el b/init.el index 4b7bec3..6b84330 100644 --- a/init.el +++ b/init.el @@ -2000,7 +2000,9 @@ If no name is given, list all bookmarks instead." (use-package eshell-starship :ensure nil :demand t - :hook (eshell-prompt-mode . eshell-starship-prompt-mode)) + :hook (eshell-prompt-mode . eshell-starship-prompt-mode) + :config + (eshell-starship-setup-evil-keybindings)) ;; proced (use-package proced @@ -2429,7 +2431,9 @@ one of the normal rainbow-delimiters-depth-N-face faces." ahs-definition-face 'bold ahs-definition-face-unfocused 'bold ahs-plugin-default-face 'bold - ahs-plugin-default-face-unfocused 'bold)) + ahs-plugin-default-face-unfocused 'bold) + :config + (keymap-unset auto-highlight-symbol-mode-map "C-x C-a" t)) ;; Theme (doom-themes) (use-package doom-themes