;;; +eshell.el -*- lexical-binding: t; -*-

;;; Code:

;; https://karthinks.com/software/jumping-directories-in-eshell/
(defun eshell/z (&optional regexp)
  "Navigate to a previously visited directory in eshell, or to
any directory proferred by `consult-dir'."
  (let ((eshell-dirs (delete-dups
                      (mapcar 'abbreviate-file-name
                              (ring-elements eshell-last-dir-ring)))))
    (cond
     ((and (not regexp) (featurep 'consult-dir))
      (let* ((consult-dir--source-eshell `(:name "Eshell"
                                                 :narrow ?e
                                                 :category file
                                                 :face consult-file
                                                 :items ,eshell-dirs))
             (consult-dir-sources (cons consult-dir--source-eshell
                                        consult-dir-sources)))
        (eshell/cd (substring-no-properties
                    (consult-dir--pick "Switch directory: ")))))
     (t (eshell/cd (if regexp (eshell-find-previous-directory regexp)
                     (completing-read "cd: " eshell-dirs)))))))

;;; Start and quit

;; from https://old.reddit.com/r/emacs/comments/1zkj2d/advanced_usage_of_eshell/
(defun +eshell-here ()
  "Go to eshell and set current directory to current buffer's."
  ;; consider: make a new eshell buffer when given a prefix argument.
  (interactive)
  (let ((dir (file-name-directory (or (buffer-file-name)
                                      default-directory))))
    (eshell)
    (eshell/pushd ".")
    (cd dir)
    (goto-char (point-max))
    (eshell-kill-input)
    (eshell-send-input)
    (setq-local scroll-margin 0)
    (recenter 0)))

(defun +eshell-quit-or-delete-char (arg)
  "Delete the character to the right, or quit eshell on an empty line."
  (interactive "p")
  (if (and (eolp) (looking-back eshell-prompt-regexp))
      (progn (eshell-life-is-too-much)
             (when (and (<= 1 (count-windows))
                        ;; TODO: This is not what I want.  What I really want is
                        ;; for an eshell-only frame (i.e., called from a
                        ;; keybind) to delete itself, but a regular Emacs frame
                        ;; with Eshell inside to stick around.  I think I'll
                        ;; need to make a frame-local (?) variable for that to
                        ;; work.
                        (> (length (frame-list)) 2)
                        server-process)
               (delete-frame)))
    (delete-forward-char arg)))

;;; Insert previous arguments
;; Record arguments

(defvar eshell-arg-history nil)
(defvar eshell-arg-history-index nil)
(add-to-list 'savehist-additional-variables 'eshell-arg-history)

(defun eshell-record-args (&rest _)
  "Record unique arguments onto the front of `eshell-arg-history'."
  (setq eshell-arg-history
        (cl-loop with history = eshell-arg-history
                 for arg in (reverse eshell-last-arguments)
                 do (setq history (cons arg (remove arg history)))
                 finally return history)))

(defun eshell-insert-prev-arg ()
  "Insert an argument from `eshell-arg-history' at point."
  (interactive)
  (if (eq last-command 'eshell-insert-prev-arg)
      (progn
        (let ((pos (point)))
          (eshell-backward-argument 1)
          (delete-region (point) pos))
        (if-let ((text (nth eshell-arg-history-index
                            eshell-arg-history)))
            (progn
              (insert text)
              (cl-incf eshell-arg-history-index))
          (insert (cl-first eshell-arg-history))
          (setq eshell-arg-history-index 1)))
    (insert (cl-first eshell-arg-history))
    (setq eshell-arg-history-index 1)))

;;;###autoload
(define-minor-mode eshell-arg-hist-mode
  "Minor mode to enable argument history, like bash/zsh with M-."
  :lighter "$."
  :keymap (let ((map (make-sparse-keymap)))
            (define-key map (kbd "M-.") #'eshell-insert-prev-arg)
            map)
  (if eshell-arg-hist-mode
      (add-hook 'eshell-post-command-hook #'eshell-record-args nil t)
    (remove-hook 'eshell-post-command-hook #'eshell-record-args t)))

;;;###autoload
(defmacro +eshell-eval-after-load (&rest forms)
  "Execute FORMS after Eshell is loaded.
If Eshell is already loaded in the session, immediately execute
forms.

I wrote this because Eshell doesn't properly do loading or
something, it's really annoying to work with."
  (declare (indent 0))
  `(progn
       (defun +eshell@setup ()
         "Setup the Eshell session."
         ,@forms)
       (when (featurep 'eshell)
         `(dolist (buf (buffer-list))
               (with-current-buffer buf
                 (when (derived-mode-p 'eshell-mode)
                   (+eshell@setup)))))
       (add-hook 'eshell-mode-hook #'+eshell@setup)))

(provide '+eshell)
;;; +eshell.el ends here
