diff --git a/init.el b/init.el index 48f009d..b1e7a5b 100644 --- a/init.el +++ b/init.el @@ -541,33 +541,25 @@ visual states." ;; project.el (use-package project - :bind (("C-c v" . my/project-vterm-or-default) + :bind (("C-c v" . my/project-eshell-or-default) :map project-prefix-map - ("s" . my/project-vterm) + ("s" . my/project-eshell) ("u" . my/project-run)) :init - (defvar my/project-vterm-hash-table (make-hash-table :test 'equal) - "Hash table that maps project root dirs to vterm buffers.") - (defun my/project-vterm (prompt) - "Switch to or create a vterm buffer in the current projects root." - (interactive (list t)) + (defvar eshell-buffer-name) + (defun my/project-eshell (prompt &optional arg) + "Switch to or create an eshell buffer in the current projects root." + (interactive (list t current-prefix-arg)) (if-let ((proj (project-current prompt)) - (default-directory (project-root proj))) - (if-let ((vterm-buff (gethash default-directory - my/project-vterm-hash-table)) - ((buffer-live-p vterm-buff))) - (switch-to-buffer vterm-buff) - (puthash default-directory - (vterm (concat "*vterm for project " default-directory "*")) - my/project-vterm-hash-table)))) - (defun my/project-vterm-or-default () - "Open a vterm for the current project, otherwise, open a normal vterm." - (interactive) - (unless (my/project-vterm nil) - (if-let ((vterm-buff (gethash nil my/project-vterm-hash-table)) - ((buffer-live-p vterm-buff))) - (switch-to-buffer vterm-buff) - (puthash nil (vterm vterm-buffer-name) my/project-vterm-hash-table)))) + (default-directory (project-root proj)) + (eshell-buffer-name + (concat "*eshell for project " default-directory "*"))) + (eshell))) + (defun my/project-eshell-or-default (&optional arg) + "Open an eshell for the current project, otherwise, open a normal eshell." + (interactive "P") + (unless (my/project-eshell nil arg) + (eshell arg))) (defvar my/project-run-command nil "Command to run with `my/project-run'.") (put 'my/project-run-command 'safe-local-variable (lambda (val) @@ -676,7 +668,30 @@ COMMAND and COMINT are like `compile'." ;; vterm (use-package vterm - :hook (vterm-mode . with-editor-export-editor)) + :hook (vterm-mode . with-editor-export-editor) + :init + (defvar my/project-vterm-hash-table (make-hash-table :test 'equal) + "Hash table that maps project root dirs to vterm buffers.") + (defun my/project-vterm (prompt) + "Switch to or create a vterm buffer in the current projects root." + (interactive (list t)) + (if-let ((proj (project-current prompt)) + (default-directory (project-root proj))) + (if-let ((vterm-buff (gethash default-directory + my/project-vterm-hash-table)) + ((buffer-live-p vterm-buff))) + (switch-to-buffer vterm-buff) + (puthash default-directory + (vterm (concat "*vterm for project " default-directory "*")) + my/project-vterm-hash-table)))) + (defun my/project-vterm-or-default () + "Open a vterm for the current project, otherwise, open a normal vterm." + (interactive) + (unless (my/project-vterm nil) + (if-let ((vterm-buff (gethash nil my/project-vterm-hash-table)) + ((buffer-live-p vterm-buff))) + (switch-to-buffer vterm-buff) + (puthash nil (vterm vterm-buffer-name) my/project-vterm-hash-table))))) ;; eat (mostly for eshell purposes) (use-package eat) @@ -685,53 +700,180 @@ COMMAND and COMINT are like `compile'." (use-package eshell :ensure nil :defer nil - :hook ((eshell-mode . my/-eshell-local-init-alias-hook) - (eshell-mode . my/eshell-update-aliases) - (eshell-load . eat-eshell-visual-command-mode) - (eshell-load . eat-eshell-mode)) + :hook ((eshell-load . eat-eshell-visual-command-mode) + (eshell-load . eat-eshell-mode) + (eshell-mode . my/-eshell-mode-setup)) :init + (defun my/-eshell-mode-setup () + "Setup function run from `eshell-mode-hook'" + (setq-local corfu-auto nil)) (setq-default eshell-command-aliases-list - '(("clear" "clear t" 0 7 (escaped t)))) - (defvar my/eshell-aliases - '(("ls" . "eza --git -F") - ("la" . "ls -a")) - "Aliases for eshell that work better than the default.") - (defun my/-eshell-resolve-alias (name) - "Recursively resolve an alias, NAME, from `my/eshell-aliases'." - (if-let ((entry (assoc name my/eshell-aliases)) - (def (split-string-and-unquote (cdr entry)))) - (if-let (sub-def (my/-eshell-resolve-alias (car def))) - (append sub-def (cdr def)) - def))) - (defun my/eshell-update-aliases () - "Update aliases in `my/eshell-aliases'." - (interactive) - (dolist (entry my/eshell-aliases) - (when-let ((sym (intern (concat "eshell/" (car entry)))) - (def (my/-eshell-resolve-alias (car entry)))) - (defalias sym - #'(lambda (&rest args) - (eshell-flush -1) - (throw 'eshell-external - (eshell-external-command (car def) - (append (cdr def) args))) - (eshell-flush)) - (concat "Eshell alias for \"" (s-join " " def) "\""))))) - (defun my/-eshell-local-init-alias-hook () - "Run from `eshell-mode-hook' to initialize aliases." - (dolist (entry my/eshell-aliases) - (add-to-list 'eshell-complex-commands (car entry)))) - (defun my/-eshell-lookup-my-alias-first (oldfun name) - "Advice around `eshell-lookup-alias' to also lookup aliases from `my/eshell-aliases'." - (if-let (def (my/-eshell-resolve-alias name)) - (list name (s-join " " def)) - (funcall oldfun name))) - (advice-add 'eshell-lookup-alias :around #'my/-eshell-lookup-my-alias-first) - (defun my/-eshell-ignore-alias-file (oldfun) - "Ignore aliases in `eshell-aliases-file'" - (let ((eshell-command-aliases-list nil)) - (funcall oldfun))) - (advice-add 'eshell-alias-initialize :around #'my/-eshell-ignore-alias-file)) + '(("clear" "clear t") + ("e" "find-file $1") + ("n" "find-file $1") + ("emacs" "find-file $1") + ("nvim" "find-file $1") + ("ls" "eza --git -F $*") + ("la" "ls -a $*") + ("l" "ls -l $*") + ("ll" "la -l $*") + ("gt" "git status $*") + ("gp" "git push $*") + ("gu" "git pull $*") + ("gf" "git fetch $*") + ("ga" "git add $*") + ("gcm" "git commit -m ${string-join $* \" \"}"))) + (defun eshell/bm (&optional name) + "Change to directory of bookmark NAME. +If no name is given, list all bookmarks instead." + (if name + (eshell/cd (bookmark-get-filename name)) + (eshell-print (string-join (bookmark-all-names) " ")))) + (defun my/-replace-home-with-tilda (path) + (let ((home (getenv "HOME"))) + (if (equal home path) + "~" + (setq home (file-name-as-directory home)) + (if (string-prefix-p home path) + (concat "~/" (seq-subseq path (length home))) + path)))) + (defun my/-eshell-prompt-cut-path (num path) + "Cut PATH down to NUM components." + (let ((parts (string-split path "/" t nil))) + (concat + (when (and (file-name-absolute-p path) + (not (equal "~" (car parts))) + (<= (length parts) num)) + "/") + (string-join (last parts num) "/")))) + (defun my/-eshell-prompt-get-dir () + "Get dir for `my/-eshell-prompt-function'" + (my/-eshell-prompt-cut-path + 3 (if-let ((worktree (car-safe (car-safe (magit-list-worktrees)))) + (parent (file-name-parent-directory worktree))) + (file-relative-name default-directory parent) + (my/-replace-home-with-tilda default-directory)))) + (defun my/-eshell-prompt-status-char-for-branch (branch remote) + "Get the status char representing the relation between BRANCH and REMOTE." + (let ((lines (process-lines vc-git-program + "rev-list" + "--left-right" + (concat branch "..." remote))) + (to-remote nil) + (to-local nil)) + (dolist (line lines) + (if-let (((not (string-empty-p line))) + (dir-char (aref line 0))) + (if (= dir-char ?<) + (setq to-local t) + (setq to-remote t)))) + (cond + ((and to-remote to-local) ?󰹺) + (to-remote ?󰜷) + (to-local ?󰜮)))) + (defun my/-eshell-prompt-current-branch-status () + "Get the status char for the current branch and its remote." + (let ((refs (process-lines vc-git-program + "for-each-ref" + "--format=%(HEAD)%00%(refname:short)%00%(upstream:short)" + "refs/heads"))) + (catch 'break + (dolist (ref refs) + (if-let ((split-ref (split-string ref "\0" nil nil)) + ((equal (car split-ref) "*"))) + (throw 'break (my/-eshell-prompt-status-char-for-branch + (cadr split-ref) + (caddr split-ref)))))))) + (defun my/-eshell-prompt-git-state-chars () + "Get chars, like + and ✘ for `my/-eshell-prompt-function'." + (let ((lines (process-lines vc-git-program "status" "--porcelain=v1")) + (branch-status (my/-eshell-prompt-current-branch-status)) + (status-arr)) + (dolist (line lines) + (cl-loop with fields = (string-split line " " t " *") + with status-str = (car-safe fields) + for status-char across status-str + do + (cond ((or (= status-char ?M) (= status-char ?T)) + (add-to-list 'status-arr ?!)) + ((= status-char ??) + (add-to-list 'status-arr ??)) + ((or (= status-char ?A) (= status-char ?C)) + (add-to-list 'status-arr ?+)) + ((= status-char ?D) + (add-to-list 'status-arr ?)) + ((= status-char ?R) + (add-to-list 'status-arr ?»))))) + (sort status-arr #'<) + (when branch-status + (push branch-status status-arr)) + (apply 'string status-arr))) + (defun my/-eshell-prompt-git-status () + "Get git status for `my/-eshell-prompt-function'" + (let ((branch (car (vc-git-branches))) + (state (my/-eshell-prompt-git-state-chars))) + (concat + (propertize (concat " 󰊢 " branch) 'face '(:foreground "medium purple")) + (unless (string-empty-p state) + (propertize (concat " [" state "]") 'face '(:foreground "red")))))) + (defun my/-eshell-prompt-vc-status () + "Get vc status for `my/-eshell-prompt-function'." + (if-let (backend (vc-responsible-backend default-directory t)) + (if (eq backend 'Git) + (my/-eshell-prompt-git-status) + (my/-eshell-prompt-set-face-color + (concat "  " (downcase (symbol-name backend))) + "purple")))) + (defvar-local my/-eshell-prompt-last-start-time nil + "Start time of last eshell command.") + (defun my/-eshell-prompt-timer-pre-cmd () + "Command run before each eshell program to record the time." + (setq my/-eshell-prompt-last-start-time (current-time))) + (add-hook 'eshell-pre-command-hook #'my/-eshell-prompt-timer-pre-cmd) + (defun my/-eshell-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 my/-eshell-prompt-last-command-time (end-time) + "Return the prompt component for the time of the last command." + (if-let ((my/-eshell-prompt-last-start-time) + (len (time-subtract end-time + my/-eshell-prompt-last-start-time)) + (float-len (float-time len)) + ((< 3 float-len)) + (int-len (round float-len))) + (concat " time " + (propertize (my/-eshell-prompt-format-span int-len) + 'face '(:foreground "gold1"))))) + (defun my/-eshell-prompt-function () + "Function for `eshell-prompt-function'" + (let* ((end-time (current-time)) + (dir (my/-eshell-prompt-get-dir)) + (prompt (concat + "\n" + (propertize dir 'face '(:foreground "dark turquoise")) + (unless (file-writable-p dir) + " ") + (my/-eshell-prompt-vc-status) + (my/-eshell-prompt-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 my/-eshell-prompt-last-start-time nil) + prompt)) + (setq eshell-prompt-function #'my/-eshell-prompt-function + eshell-prompt-regexp "^❯ " + eshell-highlight-prompt nil)) (use-package esh-help :hook (eshell-mode . my/-setup-eshell-help-func) :init @@ -739,8 +881,6 @@ COMMAND and COMINT are like `compile'." (eldoc-mode 1) (setq-local evil-lookup-func #'esh-help-run-help)) (setup-esh-help-eldoc)) -(use-package esh-autosuggest - :hook (eshell-mode . esh-autosuggest-mode)) (use-package eshell-syntax-highlighting :init (eshell-syntax-highlighting-global-mode 1))